mirror of
https://github.com/corda/corda.git
synced 2025-06-12 20:28:18 +00:00
CORDA-2361: Split samples into contracts and workflows (#4575)
This commit is contained in:
committed by
GitHub
parent
82f5a756fe
commit
35acbc8107
@ -1,112 +0,0 @@
|
||||
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.NotarySpec
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
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)}")
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
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)}""")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
Binary file not shown.
@ -1 +0,0 @@
|
||||
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.
@ -1,34 +0,0 @@
|
||||
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))
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user