CORDA-2361: Split samples into contracts and workflows (#4575)

This commit is contained in:
Katarzyna Streich
2019-01-23 13:26:33 +00:00
committed by GitHub
parent 82f5a756fe
commit 35acbc8107
73 changed files with 562 additions and 383 deletions

View File

@ -0,0 +1,111 @@
package net.corda.traderdemo
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.millis
import net.corda.finance.DOLLARS
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.services.Permissions.Companion.all
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.testing.core.*
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.InProcess
import net.corda.testing.driver.OutOfProcess
import net.corda.testing.driver.driver
import net.corda.testing.node.TestCordapp
import net.corda.testing.node.User
import net.corda.testing.node.internal.FINANCE_CORDAPPS
import net.corda.testing.node.internal.poll
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
import net.corda.traderdemo.flow.SellerFlow
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.util.concurrent.Executors
class TraderDemoTest {
@Test
fun `runs trader demo`() {
val demoUser = User("demo", "demo", setOf(startFlow<SellerFlow>(), all()))
val bankUser = User("user1", "test", permissions = setOf(
startFlow<CashIssueFlow>(),
startFlow<CashPaymentFlow>(),
startFlow<CommercialPaperIssueFlow>(),
all()))
driver(DriverParameters(
startNodesInProcess = true,
inMemoryDB = false,
cordappsForAllNodes = FINANCE_CORDAPPS + TestCordapp.findCordapp("net.corda.traderdemo")
)) {
val (nodeA, nodeB, bankNode) = listOf(
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(demoUser)),
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)),
startNode(providedName = BOC_NAME, rpcUsers = listOf(bankUser))
).map { (it.getOrThrow() as InProcess) }
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
val client = CordaRPCClient(it.rpcAddress)
client.start(demoUser.username, demoUser.password).proxy
}
val nodeBankRpc = let {
val client = CordaRPCClient(bankNode.rpcAddress)
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)
clientBank.runIssuer(amount = 100.DOLLARS, buyerName = nodeA.services.myInfo.singleIdentity().name, sellerName = nodeB.services.myInfo.singleIdentity().name)
clientB.runSeller(buyerName = nodeA.services.myInfo.singleIdentity().name, amount = 5.DOLLARS)
assertThat(clientA.cashCount).isGreaterThan(originalACash)
assertThat(clientB.cashCount).isEqualTo(expectedBCash)
// Wait until A receives the commercial paper
val executor = Executors.newScheduledThreadPool(1)
poll(executor, "A to be notified of the commercial paper", pollInterval = 100.millis) {
val actualPaper = listOf(clientA.commercialPaperCount, clientB.commercialPaperCount)
if (actualPaper == expectedPaper) Unit else null
}.getOrThrow()
executor.shutdown()
assertThat(clientA.dollarCashBalance).isEqualTo(95.DOLLARS)
assertThat(clientB.dollarCashBalance).isEqualTo(5.DOLLARS)
}
}
@Test
fun `Test restart node during flow works properly`() {
driver(DriverParameters(
startNodesInProcess = false,
inMemoryDB = false,
cordappsForAllNodes = FINANCE_CORDAPPS + TestCordapp.findCordapp("net.corda.traderdemo")
)) {
val demoUser = User("demo", "demo", setOf(startFlow<SellerFlow>(), all()))
val bankUser = User("user1", "test", permissions = setOf(all()))
val (nodeA, nodeB, bankNode) = listOf(
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(demoUser)),
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = listOf(demoUser)),
startNode(providedName = BOC_NAME, rpcUsers = listOf(bankUser))
).map { (it.getOrThrow() as OutOfProcess) }
val nodeBRpc = CordaRPCClient(nodeB.rpcAddress).start(demoUser.username, demoUser.password).proxy
val nodeARpc = CordaRPCClient(nodeA.rpcAddress).start(demoUser.username, demoUser.password).proxy
val nodeBankRpc = let {
val client = CordaRPCClient(bankNode.rpcAddress)
client.start(bankUser.username, bankUser.password).proxy
}
TraderDemoClientApi(nodeBankRpc).runIssuer(amount = 100.DOLLARS, buyerName = nodeA.nodeInfo.singleIdentity().name, sellerName = nodeB.nodeInfo.singleIdentity().name)
val stxFuture = nodeBRpc.startFlow(::SellerFlow, nodeA.nodeInfo.singleIdentity(), 5.DOLLARS).returnValue
nodeARpc.stateMachinesFeed().updates.toBlocking().first() // wait until initiated flow starts
nodeA.stop()
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = listOf(demoUser), customOverrides = mapOf("p2pAddress" to nodeA.p2pAddress.toString()))
stxFuture.getOrThrow()
}
}
}

View File

@ -0,0 +1,69 @@
package net.corda.traderdemo
import joptsimple.OptionParser
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.finance.DOLLARS
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_BANK_B_NAME
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 {
BANK,
SELLER
}
companion object {
private val logger = contextLogger()
val buyerName = DUMMY_BANK_A_NAME
val sellerName = DUMMY_BANK_B_NAME
const val sellerRpcPort = 10009
const val bankRpcPort = 10012
}
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. We intentionally use large amounts here.
val role = options.valueOf(roleArg)!!
if (role == Role.BANK) {
val bankHost = NetworkHostAndPort("localhost", bankRpcPort)
CordaRPCClient(bankHost).use("demo", "demo") {
TraderDemoClientApi(it.proxy).runIssuer(1_100_000_000_000.DOLLARS, buyerName, sellerName)
}
} else {
val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort)
CordaRPCClient(sellerHost).use("demo", "demo") {
TraderDemoClientApi(it.proxy).runSeller(1_000_000_000_000.DOLLARS, buyerName)
}
}
}
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)
}
}

View File

@ -0,0 +1,96 @@
package net.corda.traderdemo
import net.corda.core.contracts.Amount
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.Emoji
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.messaging.vaultQueryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.builder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.finance.DOLLARS
import net.corda.finance.USD
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.contracts.getCashBalance
import net.corda.finance.flows.CashIssueFlow
import net.corda.finance.flows.CashPaymentFlow
import net.corda.node.services.vault.VaultSchemaV1
import net.corda.testing.internal.vault.VaultFiller.Companion.calculateRandomlySizedAmounts
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
import net.corda.traderdemo.flow.SellerFlow
import java.util.*
/**
* Interface for communicating with nodes running the trader demo.
*/
class TraderDemoClientApi(val rpc: CordaRPCOps) {
val cashCount: Long
get() {
val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() }
val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count)
return rpc.vaultQueryBy<Cash.State>(countCriteria).otherResults.single() as Long
}
val dollarCashBalance: Amount<Currency> get() = rpc.getCashBalance(USD)
val commercialPaperCount: Long
get() {
val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() }
val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count)
return rpc.vaultQueryBy<CommercialPaper.State>(countCriteria).otherResults.single() as Long
}
fun runIssuer(amount: Amount<Currency>, buyerName: CordaX500Name, sellerName: CordaX500Name) {
val ref = OpaqueBytes.of(1)
val buyer = rpc.wellKnownPartyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName")
val seller = rpc.wellKnownPartyFromX500Name(sellerName) ?: throw IllegalStateException("Don't know $sellerName")
val notaryIdentity = rpc.notaryIdentities().first()
val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random())
rpc.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(1), notaryIdentity).returnValue.getOrThrow()
// Pay random amounts of currency up to the requested amount
amounts.forEach { pennies ->
// TODO This can't be done in parallel, perhaps due to soft-locking issues?
rpc.startFlow(::CashPaymentFlow, amount.copy(quantity = pennies), buyer).returnValue.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.
rpc.startFlow(::CommercialPaperIssueFlow, amount, ref, seller, notaryIdentity).returnValue.getOrThrow()
println("Commercial paper issued to seller")
}
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, buyerName: CordaX500Name) {
val otherParty = rpc.wellKnownPartyFromX500Name(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
// 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(::SellerFlow, otherParty, amount).returnValue.getOrThrow()
println("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
}
}

View File

@ -0,0 +1,74 @@
package net.corda.traderdemo
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.node.services.TransactionStorage
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import java.util.*
import java.util.concurrent.Callable
/**
* Given a map of transaction id to [SignedTransaction], performs a breadth first search of the dependency graph from
* the starting point down in order to find transactions that match the given query criteria.
*
* Currently, only one kind of query is supported: find any transaction that contains a command of the given type.
*
* In future, this should support restricting the search by time, and other types of useful query.
*
* @property transactions map of transaction id to [SignedTransaction].
* @property startPoints transactions to use as starting points for the search.
* @property query query to test transactions within the graph for matching.
*/
class TransactionGraphSearch(private val transactions: TransactionStorage,
private val startPoints: List<WireTransaction>,
private val query: Query) : Callable<List<WireTransaction>> {
/**
* Query criteria to match transactions against.
*
* @property withCommandOfType contract command class to restrict matches to, or null for no filtering by command. Matches the class or
* any subclass.
* @property followInputsOfType contract output state class to follow the corresponding inputs to. Matches this exact class only.
*/
data class Query(
val withCommandOfType: Class<out CommandData>? = null,
val followInputsOfType: Class<out ContractState>? = null
) {
/**
* Test if the given transaction matches this query. Currently only supports checking if the transaction that
* contains a command of the given type.
*/
fun matches(tx: WireTransaction): Boolean {
if (withCommandOfType != null) {
if (tx.commands.any { it.value.javaClass.isAssignableFrom(withCommandOfType) })
return true
}
return false
}
}
override fun call(): List<WireTransaction> {
val alreadyVisited = HashSet<SecureHash>()
val next = ArrayList<WireTransaction>(startPoints)
val results = ArrayList<WireTransaction>()
while (next.isNotEmpty()) {
val tx = next.removeAt(next.lastIndex)
if (query.matches(tx))
results += tx
val inputsLeadingToUnvisitedTx: Iterable<StateRef> = tx.inputs.filter { it.txhash !in alreadyVisited }
val unvisitedInputTxs: Map<SecureHash, SignedTransaction> = inputsLeadingToUnvisitedTx.map { it.txhash }.toHashSet().mapNotNull { transactions.getTransaction(it) }.associateBy { it.id }
val unvisitedInputTxsWithInputIndex: Iterable<Pair<SignedTransaction, Int>> = inputsLeadingToUnvisitedTx.filter { it.txhash in unvisitedInputTxs.keys }.map { Pair(unvisitedInputTxs[it.txhash]!!, it.index) }
next += (unvisitedInputTxsWithInputIndex.filter { (stx, idx) ->
query.followInputsOfType == null || stx.tx.outputs[idx].data.javaClass == query.followInputsOfType
}.map { it.first }.filter { stx -> alreadyVisited.add(stx.id) }.map { it.tx })
}
return results
}
}

View File

@ -0,0 +1,45 @@
package net.corda.traderdemo.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.identity.Party
import net.corda.core.internal.Emoji
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.flows.TwoPartyTradeFlow
import java.util.*
@InitiatedBy(SellerFlow::class)
open class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic<SignedTransaction>() {
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)
@Suspendable
override fun call(): SignedTransaction {
progressTracker.currentStep = STARTING_BUY
// Receive the offered amount and automatically agree to it (in reality this would be a longer negotiation)
val amount = otherSideSession.receive<Amount<Currency>>().unwrap { it }
require(serviceHub.networkMapCache.notaryIdentities.isNotEmpty()) { "No notary nodes registered" }
val notary: Party = serviceHub.networkMapCache.notaryIdentities.first()
val buyer = TwoPartyTradeFlow.Buyer(
otherSideSession,
notary,
amount,
CommercialPaper.State::class.java)
// This invokes the trading flow and out pops our finished transaction.
val tradeTX: SignedTransaction = subFlow(buyer)
println("Purchase complete - we are a happy customer! Final transaction is: " +
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
return tradeTX
}
}

View File

@ -0,0 +1,80 @@
package net.corda.traderdemo.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.*
import net.corda.core.identity.Party
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 net.corda.finance.`issued by`
import net.corda.finance.contracts.CommercialPaper
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.
*/
@StartableByRPC
@InitiatingFlow
class CommercialPaperIssueFlow(private val amount: Amount<Currency>,
private val issueRef: OpaqueBytes,
private val recipient: Party,
private 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 issuance: SignedTransaction = run {
val tx = CommercialPaper().generateIssue(ourIdentity.ref(issueRef), amount `issued by` ourIdentity.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, emptyList()))
}
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
return run {
val builder = TransactionBuilder(notary)
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient)
val stx = serviceHub.signInitialTransaction(builder)
val recipientSession = initiateFlow(recipient)
subFlow(FinalityFlow(stx, listOf(recipientSession)))
}
}
}
@InitiatedBy(CommercialPaperIssueFlow::class)
class CommercialPaperIssueResponderFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
// Record the move transaction
subFlow(ReceiveFinalityFlow(otherSideSession))
}
}

View File

@ -0,0 +1,48 @@
package net.corda.traderdemo.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.internal.Emoji
import net.corda.core.transactions.SignedTransaction
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.contracts.getCashBalances
import net.corda.traderdemo.TransactionGraphSearch
@InitiatedBy(SellerFlow::class)
class LoggingBuyerFlow(private val otherSideSession: FlowSession) : BuyerFlow(otherSideSession) {
@Suspendable
override fun call(): SignedTransaction {
val tradeTX = super.call()
logIssuanceAttachment(tradeTX)
logBalance()
return tradeTX
}
private fun logBalance() {
val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" }
println("Remaining balance: ${balances.joinToString()}")
}
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
// Find the original CP issuance.
// TODO: This is potentially very expensive, and requires transaction details we may no longer have once
// SGX is enabled. Should be replaced with including the attachment on all transactions involving
// the state.
val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx),
TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
followInputsOfType = CommercialPaper.State::class.java))
val cpIssuance = search.call().single()
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
cpIssuance.attachments.first().let {
println("""
The issuance of the commercial paper came with an attachment with hash $it.
${Emoji.renderIfSupported(cpIssuance)}""")
}
}
}

View File

@ -0,0 +1,59 @@
package net.corda.traderdemo.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Amount
import net.corda.core.crypto.SecureHash
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.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.finance.contracts.CommercialPaper
import net.corda.finance.flows.TwoPartyTradeFlow
import java.util.*
@InitiatingFlow
@StartableByRPC
class SellerFlow(private val otherParty: Party,
private val amount: Amount<Currency>,
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
constructor(otherParty: Party, amount: Amount<Currency>) : this(otherParty, amount, tracker())
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 flow") {
override fun childProgressTracker(): ProgressTracker = TwoPartyTradeFlow.Seller.tracker()
}
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingFlow 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 cpOwner = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
val commercialPaper = serviceHub.vaultService.queryBy(CommercialPaper.State::class.java)
.states.firstOrNull() ?: throw IllegalStateException("No commercial paper found. Please check if you issued the papers first, follow the README for instructions.")
progressTracker.currentStep = TRADING
// Send the offered amount.
val session = initiateFlow(otherParty)
session.send(amount)
val seller = TwoPartyTradeFlow.Seller(
session,
commercialPaper,
amount,
cpOwner,
progressTracker.getChildProgressTracker(TRADING)!!)
return subFlow(seller)
}
}

View File

@ -0,0 +1 @@
These certificates are used for development mode only (and are copies of those contained within the TraderDemo jar file)

View File

@ -0,0 +1,34 @@
package net.corda.traderdemo
import net.corda.core.internal.div
import net.corda.finance.flows.CashIssueFlow
import net.corda.node.services.Permissions.Companion.all
import net.corda.node.services.Permissions.Companion.startFlow
import net.corda.testing.core.BOC_NAME
import net.corda.testing.core.DUMMY_BANK_A_NAME
import net.corda.testing.core.DUMMY_BANK_B_NAME
import net.corda.testing.driver.DriverParameters
import net.corda.testing.driver.driver
import net.corda.testing.node.User
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
import net.corda.traderdemo.flow.SellerFlow
/**
* 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>) {
val permissions = setOf(
startFlow<CashIssueFlow>(),
startFlow<SellerFlow>(),
all())
val demoUser = listOf(User("demo", "demo", permissions))
driver(DriverParameters(driverDirectory = "build" / "trader-demo-nodes", waitForAllNodesToFinish = true)) {
val user = User("user1", "test", permissions = setOf(startFlow<CashIssueFlow>(),
startFlow<CommercialPaperIssueFlow>(),
startFlow<SellerFlow>()))
startNode(providedName = DUMMY_BANK_A_NAME, rpcUsers = demoUser)
startNode(providedName = DUMMY_BANK_B_NAME, rpcUsers = demoUser)
startNode(providedName = BOC_NAME, rpcUsers = listOf(user))
}
}

View File

@ -0,0 +1,97 @@
package net.corda.traderdemo
import net.corda.core.contracts.CommandData
import net.corda.core.crypto.newSecureRandom
import net.corda.core.identity.CordaX500Name
import net.corda.core.node.services.IdentityService
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.testing.contracts.DummyContract
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.dummyCommand
import net.corda.testing.internal.rigorousMock
import net.corda.testing.node.MockServices
import net.corda.testing.node.internal.MockTransactionStorage
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class TransactionGraphSearchTests {
private companion object {
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
class GraphTransactionStorage(val originTx: SignedTransaction, val inputTx: SignedTransaction) : MockTransactionStorage() {
init {
addTransaction(originTx)
addTransaction(inputTx)
}
}
fun random31BitValue(): Int = Math.abs(newSecureRandom().nextInt())
/**
* Build a pair of transactions. The first issues a dummy output state, and has a command applied, the second then
* references that state.
*
* @param command the command to add to the origin transaction.
* @param signer signer for the two transactions and their commands.
*/
fun buildTransactions(command: CommandData): GraphTransactionStorage {
val megaCorpServices = MockServices(listOf("net.corda.testing.contracts"), megaCorp, rigorousMock<IdentityService>())
val notaryServices = MockServices(listOf("net.corda.testing.contracts"), dummyNotary, rigorousMock<IdentityService>())
val originBuilder = TransactionBuilder(dummyNotary.party)
.addOutputState(DummyState(random31BitValue()), DummyContract.PROGRAM_ID)
.addCommand(command, megaCorp.publicKey)
val originPtx = megaCorpServices.signInitialTransaction(originBuilder)
val originTx = notaryServices.addSignature(originPtx)
val inputBuilder = TransactionBuilder(dummyNotary.party)
.addInputState(originTx.tx.outRef<DummyState>(0))
.addCommand(dummyCommand(megaCorp.publicKey))
val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder)
val inputTx = megaCorpServices.addSignature(inputPtx)
return GraphTransactionStorage(originTx, inputTx)
}
@Test
fun `return empty from empty`() {
val storage = buildTransactions(DummyContract.Commands.Create())
val search = TransactionGraphSearch(storage, emptyList(), TransactionGraphSearch.Query())
val expected = emptyList<WireTransaction>()
val actual = search.call()
assertEquals(expected, actual)
}
@Test
fun `return empty from no match`() {
val storage = buildTransactions(DummyContract.Commands.Create())
val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx), TransactionGraphSearch.Query())
val expected = emptyList<WireTransaction>()
val actual = search.call()
assertEquals(expected, actual)
}
@Test
fun `return origin on match`() {
val storage = buildTransactions(DummyContract.Commands.Create())
val search = TransactionGraphSearch(storage, listOf(storage.inputTx.tx), TransactionGraphSearch.Query(DummyContract.Commands.Create::class.java))
val expected = listOf(storage.originTx.tx)
val actual = search.call()
assertEquals(expected, actual)
}
}