mirror of
https://github.com/corda/corda.git
synced 2024-12-28 16:58:55 +00:00
Cash flows and unit tests
This commit is contained in:
parent
e0b684b3ea
commit
7ab94650a6
@ -0,0 +1,53 @@
|
||||
package net.corda.ptflows.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Initiates a flow that produces an Issue/Move or Exit Cash transaction.
|
||||
*/
|
||||
abstract class AbstractPtCashFlow<out T>(override val progressTracker: ProgressTracker) : FlowLogic<T>() {
|
||||
companion object {
|
||||
object GENERATING_ID : ProgressTracker.Step("Generating anonymous identities")
|
||||
object GENERATING_TX : ProgressTracker.Step("Generating transaction")
|
||||
object SIGNING_TX : ProgressTracker.Step("Signing transaction")
|
||||
object FINALISING_TX : ProgressTracker.Step("Finalising transaction")
|
||||
|
||||
fun tracker() = ProgressTracker(GENERATING_ID, GENERATING_TX, SIGNING_TX, FINALISING_TX)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
protected fun finaliseTx(tx: SignedTransaction, extraParticipants: Set<Party>, message: String): SignedTransaction {
|
||||
try {
|
||||
return subFlow(FinalityFlow(tx, extraParticipants))
|
||||
} catch (e: NotaryException) {
|
||||
throw PtCashException(message, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined signed transaction and identity lookup map, which is the resulting data from regular cash flows.
|
||||
* Specialised flows for unit tests differ from this.
|
||||
*
|
||||
* @param stx the signed transaction.
|
||||
* @param recipient the identity used for the other side of the transaction, where applicable (i.e. this is
|
||||
* null for exit transactions). For anonymous transactions this is the confidential identity generated for the
|
||||
* transaction, otherwise this is the well known identity.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Result(val stx: SignedTransaction, val recipient: AbstractParty?)
|
||||
|
||||
abstract class AbstractRequest(val amount: Amount<Currency>)
|
||||
}
|
||||
|
||||
class PtCashException(message: String, cause: Throwable) : FlowException(message, cause)
|
@ -0,0 +1,40 @@
|
||||
package net.corda.ptflows.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.ptflows.CHF
|
||||
import net.corda.ptflows.EUR
|
||||
import net.corda.ptflows.GBP
|
||||
import net.corda.ptflows.USD
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Flow to obtain cash cordapp app configuration.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class PtCashConfigDataFlow : FlowLogic<PtCashConfiguration>() {
|
||||
companion object {
|
||||
private val supportedCurrencies = listOf(USD, GBP, CHF, EUR)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): PtCashConfiguration {
|
||||
val issuableCurrencies = supportedCurrencies.mapNotNull {
|
||||
try {
|
||||
// Currently it uses checkFlowPermission to determine the list of issuable currency as a temporary hack.
|
||||
// TODO: get the config from proper configuration source.
|
||||
checkFlowPermission("corda.issuer.$it", emptyMap())
|
||||
it
|
||||
} catch (e: FlowException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
return PtCashConfiguration(issuableCurrencies, supportedCurrencies)
|
||||
}
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
data class PtCashConfiguration(val issuableCurrencies: List<Currency>, val supportedCurrencies: List<Currency>)
|
@ -0,0 +1,82 @@
|
||||
package net.corda.ptflows.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.ptflows.contracts.asset.PtCash
|
||||
import net.corda.ptflows.contracts.asset.PtCashSelection
|
||||
import net.corda.ptflows.issuedBy
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Initiates a flow that produces an cash exit transaction.
|
||||
*
|
||||
* @param amount the amount of a currency to remove from the ledger.
|
||||
* @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the
|
||||
* issuer.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class PtCashExitFlow(private val amount: Amount<Currency>,
|
||||
private val issuerRef: OpaqueBytes,
|
||||
progressTracker: ProgressTracker) : AbstractPtCashFlow<AbstractPtCashFlow.Result>(progressTracker) {
|
||||
constructor(amount: Amount<Currency>, issueRef: OpaqueBytes) : this(amount, issueRef, tracker())
|
||||
constructor(request: ExitRequest) : this(request.amount, request.issueRef, tracker())
|
||||
|
||||
companion object {
|
||||
fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the signed transaction, and a mapping of parties to new anonymous identities generated
|
||||
* (for this flow this map is always empty).
|
||||
*/
|
||||
@Suspendable
|
||||
@Throws(PtCashException::class)
|
||||
override fun call(): AbstractPtCashFlow.Result {
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder = TransactionBuilder(notary = null)
|
||||
val issuer = ourIdentity.ref(issuerRef)
|
||||
val exitStates = PtCashSelection
|
||||
.getInstance { serviceHub.jdbcSession().metaData }
|
||||
.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
||||
val signers = try {
|
||||
PtCash().generateExit(
|
||||
builder,
|
||||
amount.issuedBy(issuer),
|
||||
exitStates)
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw PtCashException("Exiting more cash than exists", e)
|
||||
}
|
||||
|
||||
// Work out who the owners of the burnt states were (specify page size so we don't silently drop any if > DEFAULT_PAGE_SIZE)
|
||||
val inputStates = serviceHub.vaultQueryService.queryBy<PtCash.State>(VaultQueryCriteria(stateRefs = builder.inputStates()),
|
||||
PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = builder.inputStates().size)).states
|
||||
|
||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
|
||||
// count as a reason to fail?
|
||||
val participants: Set<Party> = inputStates
|
||||
.mapNotNull { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.owner) }
|
||||
.toSet()
|
||||
// Sign transaction
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val tx = serviceHub.signInitialTransaction(builder, signers)
|
||||
|
||||
// Commit the transaction
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
val notarised = finaliseTx(tx, participants, "Unable to notarise exit")
|
||||
return Result(notarised, null)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class ExitRequest(amount: Amount<Currency>, val issueRef: OpaqueBytes) : AbstractRequest(amount)
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.corda.ptflows.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction).
|
||||
*
|
||||
* We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk
|
||||
* checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash
|
||||
* and immediately transfer it, so impact of this limitation is considered minimal.
|
||||
*
|
||||
* @param amount the amount of currency to issue.
|
||||
* @param issuerBankPartyRef a reference to put on the issued currency.
|
||||
* @param notary the notary to set on the output states.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class PtCashIssueAndPaymentFlow(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
val notary: Party,
|
||||
progressTracker: ProgressTracker) : AbstractPtCashFlow<AbstractPtCashFlow.Result>(progressTracker) {
|
||||
constructor(amount: Amount<Currency>,
|
||||
issueRef: OpaqueBytes,
|
||||
recipient: Party,
|
||||
anonymous: Boolean,
|
||||
notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker())
|
||||
constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker())
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Result {
|
||||
subFlow(PtCashIssueFlow(amount, issueRef, notary))
|
||||
return subFlow(PtCashPaymentFlow(amount, recipient, anonymous))
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class IssueAndPaymentRequest(amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val notary: Party,
|
||||
val anonymous: Boolean) : AbstractRequest(amount)
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package net.corda.ptflows.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.ptflows.contracts.asset.PtCash
|
||||
import net.corda.ptflows.issuedBy
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction).
|
||||
*
|
||||
* We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk
|
||||
* checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash
|
||||
* and immediately transfer it, so impact of this limitation is considered minimal.
|
||||
*
|
||||
* @param amount the amount of currency to issue.
|
||||
* @param issuerBankPartyRef a reference to put on the issued currency.
|
||||
* @param notary the notary to set on the output states.
|
||||
*/
|
||||
@StartableByRPC
|
||||
class PtCashIssueFlow(private val amount: Amount<Currency>,
|
||||
private val issuerBankPartyRef: OpaqueBytes,
|
||||
private val notary: Party,
|
||||
progressTracker: ProgressTracker) : AbstractPtCashFlow<AbstractPtCashFlow.Result>(progressTracker) {
|
||||
constructor(amount: Amount<Currency>,
|
||||
issuerBankPartyRef: OpaqueBytes,
|
||||
notary: Party) : this(amount, issuerBankPartyRef, notary, tracker())
|
||||
constructor(request: IssueRequest) : this(request.amount, request.issueRef, request.notary, tracker())
|
||||
|
||||
@Suspendable
|
||||
override fun call(): AbstractPtCashFlow.Result {
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder = TransactionBuilder(notary)
|
||||
val issuer = ourIdentity.ref(issuerBankPartyRef)
|
||||
val signers = PtCash().generateIssue(builder, amount.issuedBy(issuer), ourIdentity, notary)
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val tx = serviceHub.signInitialTransaction(builder, signers)
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
// There is no one to send the tx to as we're the only participants
|
||||
val notarised = finaliseTx(tx, emptySet(), "Unable to notarise issue")
|
||||
return Result(notarised, ourIdentity)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class IssueRequest(amount: Amount<Currency>, val issueRef: OpaqueBytes, val notary: Party) : AbstractRequest(amount)
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package net.corda.ptflows.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.confidential.SwapIdentitiesFlow
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.ptflows.contracts.asset.PtCash
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Initiates a flow that sends cash to a recipient.
|
||||
*
|
||||
* @param amount the amount of a currency to pay to the recipient.
|
||||
* @param recipient the party to pay the currency to.
|
||||
* @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties.
|
||||
* @param anonymous whether to anonymous the recipient party. Should be true for normal usage, but may be false
|
||||
* for testing purposes.
|
||||
*/
|
||||
@StartableByRPC
|
||||
open class PtCashPaymentFlow(
|
||||
val amount: Amount<Currency>,
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
progressTracker: ProgressTracker,
|
||||
val issuerConstraint: Set<Party> = emptySet()) : AbstractPtCashFlow<AbstractPtCashFlow.Result>(progressTracker) {
|
||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
|
||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||
constructor(amount: Amount<Currency>, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker())
|
||||
constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): AbstractPtCashFlow.Result {
|
||||
progressTracker.currentStep = GENERATING_ID
|
||||
val txIdentities = if (anonymous) {
|
||||
subFlow(SwapIdentitiesFlow(recipient))
|
||||
} else {
|
||||
emptyMap<Party, AnonymousParty>()
|
||||
}
|
||||
val anonymousRecipient = txIdentities[recipient] ?: recipient
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder = TransactionBuilder(notary = null)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
val (spendTX, keysForSigning) = try {
|
||||
PtCash.generateSpend(serviceHub,
|
||||
builder,
|
||||
amount,
|
||||
ourIdentityAndCert,
|
||||
anonymousRecipient,
|
||||
issuerConstraint)
|
||||
} catch (e: InsufficientBalanceException) {
|
||||
throw PtCashException("Insufficient cash for spend: ${e.message}", e)
|
||||
}
|
||||
|
||||
progressTracker.currentStep = SIGNING_TX
|
||||
val tx = serviceHub.signInitialTransaction(spendTX, keysForSigning)
|
||||
|
||||
progressTracker.currentStep = FINALISING_TX
|
||||
val notarised = finaliseTx(tx, setOf(recipient), "Unable to notarise spend")
|
||||
return Result(notarised, anonymousRecipient)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class PaymentRequest(amount: Amount<Currency>,
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
val issuerConstraint: Set<Party> = emptySet()) : AbstractRequest(amount)
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package net.corda.ptflows.contracts.flows
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.ptflows.flows.PtCashException
|
||||
import net.corda.ptflows.flows.PtCashExitFlow
|
||||
import net.corda.ptflows.flows.PtCashIssueFlow
|
||||
import net.corda.ptflows.DOLLARS
|
||||
import net.corda.ptflows.`issued by`
|
||||
import net.corda.ptflows.contracts.asset.PtCash
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.getDefaultNotary
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetwork.MockNode
|
||||
import net.corda.testing.setCordappPackages
|
||||
import net.corda.testing.unsetCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CashExitFlowTests {
|
||||
private lateinit var mockNet : MockNetwork
|
||||
private val initialBalance = 2000.DOLLARS
|
||||
private val ref = OpaqueBytes.of(0x01)
|
||||
private lateinit var bankOfCordaNode: StartedNode<MockNode>
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var notaryNode: StartedNode<MockNode>
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
setCordappPackages("net.corda.ptflows.contracts.asset")
|
||||
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin())
|
||||
val nodes = mockNet.createSomeNodes(1)
|
||||
notaryNode = nodes.notaryNode
|
||||
bankOfCordaNode = nodes.partyNodes[0]
|
||||
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
|
||||
|
||||
mockNet.runNetwork()
|
||||
notary = bankOfCordaNode.services.getDefaultNotary()
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(initialBalance, ref, notary)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
unsetCordappPackages()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exit some cash`() {
|
||||
val exitAmount = 500.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashExitFlow(exitAmount, ref)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val exitTx = future.getOrThrow().stx.tx
|
||||
val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref))
|
||||
assertEquals(1, exitTx.inputs.size)
|
||||
assertEquals(1, exitTx.outputs.size)
|
||||
val output = exitTx.outputsOfType<PtCash.State>().single()
|
||||
assertEquals(expected, output.amount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exit zero cash`() {
|
||||
val expected = 0.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashExitFlow(expected, ref)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<PtCashException> {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.corda.ptflows.contracts.flows
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.ptflows.DOLLARS
|
||||
import net.corda.ptflows.`issued by`
|
||||
import net.corda.ptflows.contracts.asset.PtCash
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.ptflows.flows.PtCashIssueFlow
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.getDefaultNotary
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetwork.MockNode
|
||||
import net.corda.testing.setCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CashIssueFlowTests {
|
||||
private lateinit var mockNet : MockNetwork
|
||||
private lateinit var bankOfCordaNode: StartedNode<MockNode>
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var notaryNode: StartedNode<MockNode>
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
setCordappPackages("net.corda.ptflows.contracts.asset")
|
||||
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin())
|
||||
val nodes = mockNet.createSomeNodes(1)
|
||||
notaryNode = nodes.notaryNode
|
||||
bankOfCordaNode = nodes.partyNodes[0]
|
||||
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
|
||||
|
||||
mockNet.runNetwork()
|
||||
notary = bankOfCordaNode.services.getDefaultNotary()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue some cash`() {
|
||||
val expected = 500.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(expected, ref, notary)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val issueTx = future.getOrThrow().stx
|
||||
val output = issueTx.tx.outputsOfType<PtCash.State>().single()
|
||||
assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `issue zero cash`() {
|
||||
val expected = 0.DOLLARS
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(expected, ref, notary)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package net.corda.ptflows.contracts.flows
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.trackBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.ptflows.DOLLARS
|
||||
import net.corda.ptflows.`issued by`
|
||||
import net.corda.ptflows.contracts.asset.PtCash
|
||||
import net.corda.ptflows.flows.PtCashException
|
||||
import net.corda.ptflows.flows.PtCashIssueFlow
|
||||
import net.corda.ptflows.flows.PtCashPaymentFlow
|
||||
import net.corda.node.internal.StartedNode
|
||||
import net.corda.testing.chooseIdentity
|
||||
import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.getDefaultNotary
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetwork.MockNode
|
||||
import net.corda.testing.setCordappPackages
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class CashPaymentFlowTests {
|
||||
private lateinit var mockNet : MockNetwork
|
||||
private val initialBalance = 2000.DOLLARS
|
||||
private val ref = OpaqueBytes.of(0x01)
|
||||
private lateinit var bankOfCordaNode: StartedNode<MockNode>
|
||||
private lateinit var bankOfCorda: Party
|
||||
private lateinit var notaryNode: StartedNode<MockNode>
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
setCordappPackages("net.corda.ptflows.contracts.asset")
|
||||
mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin())
|
||||
val nodes = mockNet.createSomeNodes(1)
|
||||
notaryNode = nodes.notaryNode
|
||||
bankOfCordaNode = nodes.partyNodes[0]
|
||||
bankOfCorda = bankOfCordaNode.info.chooseIdentity()
|
||||
notary = notaryNode.services.getDefaultNotary()
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashIssueFlow(initialBalance, ref, notary)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pay some cash`() {
|
||||
val payTo = notaryNode.info.chooseIdentity()
|
||||
val expectedPayment = 500.DOLLARS
|
||||
val expectedChange = 1500.DOLLARS
|
||||
|
||||
bankOfCordaNode.database.transaction {
|
||||
// Register for vault updates
|
||||
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
|
||||
val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy<PtCash.State>(criteria)
|
||||
val (_, vaultUpdatesBankClient) = notaryNode.services.vaultQueryService.trackBy<PtCash.State>(criteria)
|
||||
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(expectedPayment,
|
||||
payTo)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
future.getOrThrow()
|
||||
|
||||
// Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary
|
||||
// and $1,500 back to us, so we expect to consume one state, produce one state for our own vault
|
||||
vaultUpdatesBoc.expectEvents {
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" }
|
||||
require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" }
|
||||
val changeState = update.produced.single().state.data
|
||||
assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), changeState.amount)
|
||||
}
|
||||
}
|
||||
|
||||
// Check notary node vault updates
|
||||
vaultUpdatesBankClient.expectEvents {
|
||||
expect { (consumed, produced) ->
|
||||
require(consumed.isEmpty()) { consumed.size }
|
||||
require(produced.size == 1) { produced.size }
|
||||
val paymentState = produced.single().state.data
|
||||
assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pay more than we have`() {
|
||||
val payTo = notaryNode.info.chooseIdentity()
|
||||
val expected = 4000.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(expected,
|
||||
payTo)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<PtCashException> {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pay zero cash`() {
|
||||
val payTo = notaryNode.info.chooseIdentity()
|
||||
val expected = 0.DOLLARS
|
||||
val future = bankOfCordaNode.services.startFlow(PtCashPaymentFlow(expected,
|
||||
payTo)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
future.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user