diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index c2da247149..25473367e2 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -1,5 +1,6 @@ package net.corda.contracts; +import co.paralleluniverse.fibers.*; import com.google.common.collect.*; import kotlin.*; import net.corda.contracts.asset.*; @@ -311,6 +312,7 @@ public class JavaCommercialPaper implements Contract { return generateIssue(issuance, faceValue, maturityDate, notary, null); } + @Suspendable public void generateRedeem(TransactionBuilder tx, StateAndRef paper, VaultService vault) throws InsufficientBalanceException { vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null); tx.addInputState(paper); diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index b07d375394..d68f0f36a1 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -1,5 +1,6 @@ package net.corda.contracts +import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy import net.corda.contracts.clause.AbstractIssue import net.corda.core.contracts.* @@ -214,6 +215,7 @@ class CommercialPaper : Contract { * @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer. */ @Throws(InsufficientBalanceException::class) + @Suspendable fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { // Add the cash movement using the states in our vault. val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt index 651355235e..827a1bc00e 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt @@ -1,5 +1,6 @@ package net.corda.contracts +import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* import net.corda.core.crypto.NullPublicKey @@ -124,6 +125,7 @@ class CommercialPaperLegacy : Contract { } @Throws(InsufficientBalanceException::class) + @Suspendable fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { // Add the cash movement using the states in our vault. vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index 08e8830177..2c6501b251 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -209,6 +209,7 @@ object TwoPartyTradeFlow { return ptx.toSignedTransaction(checkSufficientSignatures = false) } + @Suspendable private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair> { val ptx = TransactionType.General.Builder(notary) diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 7e479e39a4..ef78eab63b 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -4,7 +4,9 @@ import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.* import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.AnonymousParty +import net.corda.core.crypto.Party +import net.corda.core.crypto.SecureHash import net.corda.core.days import net.corda.core.flows.FlowStateMachine import net.corda.core.flows.StateMachineRunId @@ -16,11 +18,7 @@ import net.corda.core.rootCause import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.LogHelper -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.core.utilities.* import net.corda.flows.TwoPartyTradeFlow.Buyer import net.corda.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.AbstractNode @@ -121,6 +119,57 @@ class TwoPartyTradeFlowTests { } } + @Test(expected = InsufficientBalanceException::class) + fun `trade cash for commercial paper fails using soft locking`() { + net = MockNetwork(false, true) + + ledger { + val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) + val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) + val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) + val aliceKey = aliceNode.services.legalIdentityKey + val notaryKey = notaryNode.services.notaryIdentityKey + + aliceNode.disableDBCloseOnStop() + bobNode.disableDBCloseOnStop() + + val cashStates = + bobNode.database.transaction { + bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3) + } + + val alicesFakePaper = aliceNode.database.transaction { + fillUpForSeller(false, aliceNode.info.legalIdentity.owningKey, + 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, null, notaryNode.info.notaryIdentity).second + } + + insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, aliceKey, notaryKey) + + val cashLockId = UUID.randomUUID() + bobNode.database.transaction { + // lock the cash states with an arbitrary lockId (to prevent the Buyer flow from claiming the states) + bobNode.vault.softLockReserve(cashLockId, cashStates.states.map { it.ref }.toSet()) + } + + val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, + "alice's paper".outputStateAndRef()) + + assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow()) + + aliceNode.stop() + bobNode.stop() + + aliceNode.database.transaction { + assertThat(aliceNode.checkpointStorage.checkpoints()).isEmpty() + } + aliceNode.manuallyCloseDB() + bobNode.database.transaction { + assertThat(bobNode.checkpointStorage.checkpoints()).isEmpty() + } + bobNode.manuallyCloseDB() + } + } + @Test fun `shutdown and restore`() { ledger {