mirror of
https://github.com/corda/corda.git
synced 2025-06-12 20:28:18 +00:00
CORDA-2672: Tidy up CorDapp deployments in samples. (#4815)
* CORDA-2672: Tidy up CorDapp deployments in samples. * CORDA-2672: Refactor Attachment Demo. * Remove Bank of Corda from Trader Demo. * Configure SLF4J simple loggers, fix comments and documentation.
This commit is contained in:
@ -0,0 +1,110 @@
|
||||
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.USD
|
||||
import net.corda.finance.workflows.getCashBalance
|
||||
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.BOC_NAME
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_BANK_B_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.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.assertCheckpoints
|
||||
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.flow")
|
||||
)) {
|
||||
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)
|
||||
try {
|
||||
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()
|
||||
} finally {
|
||||
executor.shutdownNow()
|
||||
}
|
||||
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.flow")
|
||||
)) {
|
||||
val (buyer, seller, bank) = listOf(
|
||||
startNode(providedName = DUMMY_BANK_A_NAME),
|
||||
startNode(providedName = DUMMY_BANK_B_NAME),
|
||||
startNode(providedName = BOC_NAME)
|
||||
).map { it.getOrThrow() }
|
||||
TraderDemoClientApi(bank.rpc).runIssuer(amount = 100.DOLLARS, buyerName = DUMMY_BANK_A_NAME, sellerName = DUMMY_BANK_B_NAME)
|
||||
val saleFuture = seller.rpc.startFlow(::SellerFlow, buyer.nodeInfo.singleIdentity(), 5.DOLLARS).returnValue
|
||||
buyer.rpc.stateMachinesFeed().updates.toBlocking().first() // wait until initiated flow starts
|
||||
buyer.stop()
|
||||
assertCheckpoints(DUMMY_BANK_A_NAME, 1)
|
||||
val buyer2 = startNode(providedName = DUMMY_BANK_A_NAME, customOverrides = mapOf("p2pAddress" to buyer.p2pAddress.toString())).getOrThrow()
|
||||
saleFuture.getOrThrow()
|
||||
assertThat(buyer2.rpc.getCashBalance(USD)).isEqualTo(95.DOLLARS)
|
||||
assertThat(seller.rpc.getCashBalance(USD)).isEqualTo(5.DOLLARS)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.workflows.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)}")
|
||||
}
|
||||
}
|
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,3 @@
|
||||
org.slf4j.simpleLogger.defaultLogLevel=info
|
||||
org.slf4j.simpleLogger.showDateTime=true
|
||||
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z
|
Reference in New Issue
Block a user