mirror of
https://github.com/corda/corda.git
synced 2025-06-15 13:48:14 +00:00
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:
@ -32,6 +32,9 @@ UNRELEASED
|
|||||||
of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified
|
of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified
|
||||||
by set of lock ids)
|
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
|
Milestone 14
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -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``
|
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
|
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``
|
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
|
you can see flows running on both sides of transaction. Additionally you should see final trade information displayed
|
||||||
to your terminal.
|
to your terminal.
|
||||||
|
@ -142,7 +142,8 @@ object TwoPartyTradeFlow {
|
|||||||
// Put together a proposed transaction that performs the trade, and sign it.
|
// Put together a proposed transaction that performs the trade, and sign it.
|
||||||
progressTracker.currentStep = SIGNING
|
progressTracker.currentStep = SIGNING
|
||||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest)
|
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
|
// 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.
|
// 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
|
@Suspendable
|
||||||
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
|
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
|
||||||
|
@ -40,8 +40,9 @@ dependencies {
|
|||||||
|
|
||||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||||
ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [
|
ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [
|
||||||
'StartFlow.net.corda.flows.IssuerFlow$IssuanceRequester',
|
'StartFlow.net.corda.flows.CashIssueFlow',
|
||||||
"StartFlow.net.corda.traderdemo.flow.SellerFlow"
|
'StartFlow.net.corda.traderdemo.flow.CommercialPaperIssueFlow',
|
||||||
|
'StartFlow.net.corda.traderdemo.flow.SellerFlow'
|
||||||
]]]
|
]]]
|
||||||
|
|
||||||
directory "./build/nodes"
|
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"
|
name "CN=BankOfCorda,O=R3,L=New York,C=US"
|
||||||
advertisedServices = []
|
advertisedServices = []
|
||||||
p2pPort 10011
|
p2pPort 10011
|
||||||
|
rpcPort 10012
|
||||||
cordapps = []
|
cordapps = []
|
||||||
|
rpcUsers = ext.rpcUsers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,11 +105,11 @@ publishing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task runBuyer(type: JavaExec) {
|
task runBank(type: JavaExec) {
|
||||||
classpath = sourceSets.main.runtimeClasspath
|
classpath = sourceSets.main.runtimeClasspath
|
||||||
main = 'net.corda.traderdemo.TraderDemoKt'
|
main = 'net.corda.traderdemo.TraderDemoKt'
|
||||||
args '--role'
|
args '--role'
|
||||||
args 'BUYER'
|
args 'BANK'
|
||||||
}
|
}
|
||||||
|
|
||||||
task runSeller(type: JavaExec) {
|
task runSeller(type: JavaExec) {
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.utilities.millis
|
|||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.internal.concurrent.transpose
|
import net.corda.core.internal.concurrent.transpose
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.flows.CashIssueFlow
|
||||||
import net.corda.testing.DUMMY_BANK_A
|
import net.corda.testing.DUMMY_BANK_A
|
||||||
import net.corda.testing.DUMMY_BANK_B
|
import net.corda.testing.DUMMY_BANK_B
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
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.driver.poll
|
||||||
import net.corda.testing.node.NodeBasedTest
|
import net.corda.testing.node.NodeBasedTest
|
||||||
import net.corda.traderdemo.flow.BuyerFlow
|
import net.corda.traderdemo.flow.BuyerFlow
|
||||||
|
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
|
||||||
import net.corda.traderdemo.flow.SellerFlow
|
import net.corda.traderdemo.flow.SellerFlow
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -25,15 +27,13 @@ import java.util.concurrent.Executors
|
|||||||
class TraderDemoTest : NodeBasedTest() {
|
class TraderDemoTest : NodeBasedTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `runs trader demo`() {
|
fun `runs trader demo`() {
|
||||||
val permissions = setOf(
|
val demoUser = User("demo", "demo", setOf(startFlowPermission<SellerFlow>()))
|
||||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
val bankUser = User("user1", "test", permissions = setOf(startFlowPermission<CashIssueFlow>(),
|
||||||
startFlowPermission<SellerFlow>())
|
startFlowPermission<CommercialPaperIssueFlow>()))
|
||||||
val demoUser = listOf(User("demo", "demo", permissions))
|
val (nodeA, nodeB, bankNode, notaryNode) = listOf(
|
||||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)),
|
||||||
val (nodeA, nodeB) = listOf(
|
startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)),
|
||||||
startNode(DUMMY_BANK_A.name, rpcUsers = demoUser),
|
startNode(BOC.name, rpcUsers = listOf(bankUser)),
|
||||||
startNode(DUMMY_BANK_B.name, rpcUsers = demoUser),
|
|
||||||
startNode(BOC.name, rpcUsers = listOf(user)),
|
|
||||||
startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||||
).transpose().getOrThrow()
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
@ -41,19 +41,24 @@ class TraderDemoTest : NodeBasedTest() {
|
|||||||
|
|
||||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
||||||
val client = CordaRPCClient(it.configuration.rpcAddress!!, initialiseSerialization = false)
|
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 clientA = TraderDemoClientApi(nodeARpc)
|
||||||
val clientB = TraderDemoClientApi(nodeBRpc)
|
val clientB = TraderDemoClientApi(nodeBRpc)
|
||||||
|
val clientBank = TraderDemoClientApi(nodeBankRpc)
|
||||||
|
|
||||||
val originalACash = clientA.cashCount // A has random number of issued amount
|
val originalACash = clientA.cashCount // A has random number of issued amount
|
||||||
val expectedBCash = clientB.cashCount + 1
|
val expectedBCash = clientB.cashCount + 1
|
||||||
val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount)
|
val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount)
|
||||||
|
|
||||||
// TODO: Enable anonymisation
|
// TODO: Enable anonymisation
|
||||||
clientA.runBuyer(amount = 100.DOLLARS, anonymous = false)
|
clientBank.runIssuer(amount = 100.DOLLARS, buyerName = nodeA.info.legalIdentity.name, sellerName = nodeB.info.legalIdentity.name, notaryName = notaryNode.info.legalIdentity.name)
|
||||||
clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS)
|
clientB.runSeller(buyerName = nodeA.info.legalIdentity.name, amount = 5.DOLLARS)
|
||||||
|
|
||||||
assertThat(clientA.cashCount).isGreaterThan(originalACash)
|
assertThat(clientA.cashCount).isGreaterThan(originalACash)
|
||||||
assertThat(clientB.cashCount).isEqualTo(expectedBCash)
|
assertThat(clientB.cashCount).isEqualTo(expectedBCash)
|
||||||
|
@ -4,8 +4,10 @@ import joptsimple.OptionParser
|
|||||||
import net.corda.client.rpc.CordaRPCClient
|
import net.corda.client.rpc.CordaRPCClient
|
||||||
import net.corda.core.contracts.DOLLARS
|
import net.corda.core.contracts.DOLLARS
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
import net.corda.testing.DUMMY_BANK_A
|
|
||||||
import net.corda.core.utilities.loggerFor
|
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 org.slf4j.Logger
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
@ -18,12 +20,18 @@ fun main(args: Array<String>) {
|
|||||||
|
|
||||||
private class TraderDemo {
|
private class TraderDemo {
|
||||||
enum class Role {
|
enum class Role {
|
||||||
BUYER,
|
BANK,
|
||||||
SELLER
|
SELLER
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val logger: Logger = loggerFor<TraderDemo>()
|
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>) {
|
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
|
// 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.
|
// will contact the buyer and actually make something happen.
|
||||||
val role = options.valueOf(roleArg)!!
|
val role = options.valueOf(roleArg)!!
|
||||||
if (role == Role.BUYER) {
|
if (role == Role.BANK) {
|
||||||
val host = NetworkHostAndPort("localhost", 10006)
|
val bankHost = NetworkHostAndPort("localhost", bankRpcPort)
|
||||||
CordaRPCClient(host).start("demo", "demo").use {
|
CordaRPCClient(bankHost).use("demo", "demo") {
|
||||||
TraderDemoClientApi(it.proxy).runBuyer()
|
TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName, notaryName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val host = NetworkHostAndPort("localhost", 10009)
|
val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort)
|
||||||
CordaRPCClient(host).use("demo", "demo") {
|
CordaRPCClient(sellerHost).use("demo", "demo") {
|
||||||
TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, DUMMY_BANK_A.name)
|
TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, buyerName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,11 @@ import net.corda.core.node.services.vault.builder
|
|||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
import net.corda.core.utilities.getOrThrow
|
||||||
import net.corda.core.utilities.loggerFor
|
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.node.services.vault.VaultSchemaV1
|
||||||
import net.corda.testing.BOC
|
|
||||||
import net.corda.testing.DUMMY_NOTARY
|
import net.corda.testing.DUMMY_NOTARY
|
||||||
import net.corda.testing.contracts.calculateRandomlySizedAmounts
|
import net.corda.testing.contracts.calculateRandomlySizedAmounts
|
||||||
|
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
|
||||||
import net.corda.traderdemo.flow.SellerFlow
|
import net.corda.traderdemo.flow.SellerFlow
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -47,25 +47,42 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
|
|||||||
return rpc.vaultQueryBy<CommercialPaper.State>(countCriteria).otherResults.single() as Long
|
return rpc.vaultQueryBy<CommercialPaper.State>(countCriteria).otherResults.single() as Long
|
||||||
}
|
}
|
||||||
|
|
||||||
fun runBuyer(amount: Amount<Currency> = 30000.DOLLARS, anonymous: Boolean = false) {
|
fun runIssuer(amount: Amount<Currency> = 1100.0.DOLLARS, buyerName: X500Name, sellerName: X500Name, notaryName: X500Name) {
|
||||||
val bankOfCordaParty = rpc.partyFromX500Name(BOC.name)
|
val ref = OpaqueBytes.of(1)
|
||||||
?: throw IllegalStateException("Unable to locate ${BOC.name} in Network Map Service")
|
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)
|
val notaryLegalIdentity = rpc.partyFromX500Name(DUMMY_NOTARY.name)
|
||||||
?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service")
|
?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service")
|
||||||
val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity)
|
val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity)
|
||||||
?: throw IllegalStateException("Unable to locate notary node in network map cache")
|
?: throw IllegalStateException("Unable to locate notary node in network map cache")
|
||||||
val me = rpc.nodeIdentity()
|
|
||||||
val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random())
|
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 ->
|
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()
|
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) {
|
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, buyerName: X500Name) {
|
||||||
val otherParty = rpc.partyFromX500Name(counterparty) ?: throw IllegalStateException("Don't know $counterparty")
|
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 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
|
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,25 +2,17 @@ package net.corda.traderdemo.flow
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.contracts.CommercialPaper
|
import net.corda.contracts.CommercialPaper
|
||||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
import net.corda.core.contracts.Amount
|
||||||
import net.corda.core.contracts.*
|
|
||||||
import net.corda.core.crypto.SecureHash
|
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.FlowLogic
|
||||||
import net.corda.core.flows.InitiatingFlow
|
import net.corda.core.flows.InitiatingFlow
|
||||||
import net.corda.core.flows.StartableByRPC
|
import net.corda.core.flows.StartableByRPC
|
||||||
import net.corda.core.identity.AbstractParty
|
|
||||||
import net.corda.core.identity.AnonymousParty
|
import net.corda.core.identity.AnonymousParty
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.utilities.seconds
|
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.flows.TwoPartyTradeFlow
|
import net.corda.flows.TwoPartyTradeFlow
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@InitiatingFlow
|
@InitiatingFlow
|
||||||
@ -51,7 +43,8 @@ class SellerFlow(val otherParty: Party,
|
|||||||
|
|
||||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
||||||
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
|
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
|
progressTracker.currentStep = TRADING
|
||||||
|
|
||||||
@ -66,39 +59,4 @@ class SellerFlow(val otherParty: Party,
|
|||||||
progressTracker.getChildProgressTracker(TRADING)!!)
|
progressTracker.getChildProgressTracker(TRADING)!!)
|
||||||
return subFlow(seller)
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user