From b40fee151270781d5774a0120a33fbc70e1ca35d Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 28 Jul 2016 11:51:19 +0200 Subject: [PATCH] Allow for easier conversion from Amount> to Amount and de-issuerify a few places. --- .../contracts/JavaCommercialPaper.java | 2 +- .../com/r3corda/contracts/asset/Cash.kt | 24 +++++++------------ .../r3corda/contracts/asset/FungibleAsset.kt | 4 +++- .../protocols/TwoPartyTradeProtocol.kt | 10 ++++---- .../com/r3corda/core/contracts/Structures.kt | 7 ++++++ .../node/internal/testing/TradeSimulation.kt | 5 +--- .../messaging/TwoPartyTradeProtocolTests.kt | 20 ++++++++-------- .../node/services/WalletWithCashTest.kt | 2 +- .../kotlin/com/r3corda/demos/TraderDemo.kt | 10 ++++---- 9 files changed, 41 insertions(+), 43 deletions(-) diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index 497e5a1cda..b589d0ffb6 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -344,7 +344,7 @@ public class JavaCommercialPaper extends ClauseVerifier { } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { - new Cash().generateSpend(tx, paper.getState().getData().getFaceValue(), paper.getState().getData().getOwner(), wallet); + new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), wallet, null); tx.addInputState(paper); tx.addCommand(new Command(new Commands.Redeem(paper.getState().getNotary()), paper.getState().getData().getOwner())); } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt index f2915f8ded..7a93ac0c03 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/Cash.kt @@ -211,16 +211,6 @@ class Cash : ClauseVerifier() { tx.addCommand(Cash.Commands.Issue(), at.party.owningKey) } - /** - * Generate a transaction that consumes one or more of the given input states to move money to the given pubkey. - * Note that the wallet list is not updated: it's up to you to do that. - */ - @Throws(InsufficientBalanceException::class) - fun generateSpend(tx: TransactionBuilder, amount: Amount>, to: PublicKey, - cashStates: List>): List = - generateSpend(tx, Amount(amount.quantity, amount.token.product), to, cashStates, - setOf(amount.token.issuer.party)) - /** * Generate a transaction that consumes one or more of the given input states to move money to the given pubkey. * Note that the wallet list is not updated: it's up to you to do that. @@ -301,21 +291,23 @@ class Cash : ClauseVerifier() { /** * Sums the cash states in the list belonging to a single owner, throwing an exception * if there are none, or if any of the cash states cannot be added together (i.e. are - * different currencies). + * different currencies or issuers). */ -fun Iterable.sumCashBy(owner: PublicKey) = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() +fun Iterable.sumCashBy(owner: PublicKey): Amount> = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() /** * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash * states cannot be added together (i.e. are different currencies). */ -fun Iterable.sumCash() = filterIsInstance().map { it.amount }.sumOrThrow() +fun Iterable.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() /** Sums the cash states in the list, returning null if there are none. */ -fun Iterable.sumCashOrNull() = filterIsInstance().map { it.amount }.sumOrNull() +fun Iterable.sumCashOrNull(): Amount>? = filterIsInstance().map { it.amount }.sumOrNull() -/** Sums the cash states in the list, returning zero of the given currency if there are none. */ -fun Iterable.sumCashOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero>(currency) +/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */ +fun Iterable.sumCashOrZero(currency: Issued): Amount> { + return filterIsInstance().map { it.amount }.sumOrZero>(currency) +} /** * Returns a map of how much cash we have in each currency, ignoring details like issuer. Note: currencies for diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt index 67d7e7e910..5636ab50e1 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/FungibleAsset.kt @@ -4,7 +4,9 @@ import com.r3corda.core.contracts.* import java.security.PublicKey import java.util.* -class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() +class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() { + override fun toString() = "Insufficient balance, missing $amountMissing" +} /** * Interface for contract states representing assets which are fungible, countable and issued by a diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index a9a16df05d..fe06b16bd3 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -47,7 +47,7 @@ object TwoPartyTradeProtocol { val TOPIC = "platform.trade" - class UnacceptablePriceException(val givenPrice: Amount>) : Exception() + class UnacceptablePriceException(val givenPrice: Amount) : Exception() class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() { override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName" } @@ -55,7 +55,7 @@ object TwoPartyTradeProtocol { // This object is serialised to the network and is the first protocol message the seller sends to the buyer. class SellerTradeInfo( val assetForSale: StateAndRef, - val price: Amount>, + val price: Amount, val sellerOwnerKey: PublicKey, val sessionID: Long ) @@ -66,7 +66,7 @@ object TwoPartyTradeProtocol { open class Seller(val otherSide: Party, val notaryNode: NodeInfo, val assetToSell: StateAndRef, - val price: Amount>, + val price: Amount, val myKeyPair: KeyPair, val buyerSessionID: Long, override val progressTracker: ProgressTracker = Seller.tracker()) : ProtocolLogic() { @@ -133,7 +133,7 @@ object TwoPartyTradeProtocol { // This verifies that the transaction is contract-valid, even though it is missing signatures. serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)) - if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public) != price) + if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public).withoutIssuer() != price) throw IllegalArgumentException("Transaction is not sending us the right amount of cash") // There are all sorts of funny games a malicious secondary might play here, we should fix them: @@ -178,7 +178,7 @@ object TwoPartyTradeProtocol { open class Buyer(val otherSide: Party, val notary: Party, - val acceptablePrice: Amount>, + val acceptablePrice: Amount, val typeToBuy: Class, val sessionID: Long) : ProtocolLogic() { diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt index 56b60a917e..8bd276c240 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -150,6 +150,13 @@ data class Issued(val issuer: PartyAndReference, val product: P) { override fun toString() = "$product issued by $issuer" } +/** + * Strips the issuer and returns an [Amount] of the raw token directly. This is useful when you are mixing code that + * cares about specific issuers with code that will accept any, or which is imposing issuer constraints via some + * other mechanism and the additional type safety is not wanted. + */ +fun Amount>.withoutIssuer(): Amount = Amount(quantity, token.product) + /** * A contract state that can have a single owner. */ diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt index 007a1f8d31..ad844dcc13 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/TradeSimulation.kt @@ -8,8 +8,6 @@ import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.contracts.`issued by` -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.days import com.r3corda.core.random63BitValue import com.r3corda.core.seconds @@ -43,8 +41,7 @@ class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwo } seller.services.recordTransactions(issuance) - val cashIssuerKey = generateKeyPair() - val amount = 1000.DOLLARS `issued by` Party("Big friendly bank", cashIssuerKey.public).ref(1) + val amount = 1000.DOLLARS val sessionID = random63BitValue() val buyerProtocol = TwoPartyTradeProtocol.Buyer( seller.info.identity, diff --git a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt index 99a047ae52..aec019bb17 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -54,14 +54,14 @@ class TwoPartyTradeProtocolTests { lateinit var net: MockNetwork private fun runSeller(smm: StateMachineManager, notary: NodeInfo, - otherSide: Party, assetToSell: StateAndRef, price: Amount>, + otherSide: Party, assetToSell: StateAndRef, price: Amount, myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture { val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, assetToSell, price, myKeyPair, buyerSessionID) return smm.add("${TwoPartyTradeProtocol.TOPIC}.seller", seller) } private fun runBuyer(smm: StateMachineManager, notaryNode: NodeInfo, - otherSide: Party, acceptablePrice: Amount>, typeToBuy: Class, + otherSide: Party, acceptablePrice: Amount, typeToBuy: Class, sessionID: Long): ListenableFuture { val buyer = TwoPartyTradeProtocol.Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID) return smm.add("${TwoPartyTradeProtocol.TOPIC}.buyer", buyer) @@ -106,7 +106,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceNode.info.identity, - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, CommercialPaper.State::class.java, buyerSessionID ) @@ -115,7 +115,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobNode.info.identity, "alice's paper".outputStateAndRef(), - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, ALICE_KEY, buyerSessionID ) @@ -158,7 +158,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobNode.info.identity, "alice's paper".outputStateAndRef(), - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, ALICE_KEY, buyerSessionID ) @@ -166,7 +166,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceNode.info.identity, - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, CommercialPaper.State::class.java, buyerSessionID ) @@ -279,7 +279,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobNode.info.identity, "alice's paper".outputStateAndRef(), - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, ALICE_KEY, buyerSessionID ) @@ -287,7 +287,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceNode.info.identity, - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, CommercialPaper.State::class.java, buyerSessionID ) @@ -390,7 +390,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobNode.info.identity, "alice's paper".outputStateAndRef(), - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, ALICE_KEY, buyerSessionID ) @@ -398,7 +398,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceNode.info.identity, - 1000.DOLLARS `issued by` issuer, + 1000.DOLLARS, CommercialPaper.State::class.java, buyerSessionID ) diff --git a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt index 0270e7ffa9..8f8cd5ba3b 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/WalletWithCashTest.kt @@ -76,7 +76,7 @@ class WalletWithCashTest { // A tx that spends our money. val spendTX = TransactionType.General.Builder().apply { - Cash().generateSpend(this, 80.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_PUBKEY, listOf(myOutput)) + Cash().generateSpend(this, 80.DOLLARS, BOB_PUBKEY, listOf(myOutput)) signWith(freshKey) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index ebdbde4689..c98449523b 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -164,7 +164,7 @@ fun runTraderDemo(args: Array): Int { // 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. - val amount = 1000.DOLLARS `issued by` cashIssuer.ref(0) // Note: "0" has to match the reference used in the wallet filler + val amount = 1000.DOLLARS if (role == Role.BUYER) { runBuyer(node, amount) } else { @@ -174,7 +174,7 @@ fun runTraderDemo(args: Array): Int { return 0 } -private fun runSeller(node: Node, amount: Amount>, otherSide: Party) { +private fun runSeller(node: Node, amount: Amount, otherSide: Party) { // 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 @@ -202,7 +202,7 @@ private fun runSeller(node: Node, amount: Amount>, otherSide: P node.stop() } -private fun runBuyer(node: Node, amount: Amount>) { +private fun runBuyer(node: Node, amount: Amount) { // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. // For demo purposes just extract attachment jars when saved to disk, so the user can explore them. val attachmentsPath = (node.storage.attachments as NodeAttachmentService).let { @@ -236,7 +236,7 @@ val DEMO_TOPIC = "initiate.demo.trade" private class TraderDemoProtocolBuyer(val otherSide: Party, private val attachmentsPath: Path, - val amount: Amount>, + val amount: Amount, override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : ProtocolLogic() { object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset") @@ -294,7 +294,7 @@ ${Emoji.renderIfSupported(cpIssuance)}""") } private class TraderDemoProtocolSeller(val otherSide: Party, - val amount: Amount>, + val amount: Amount, override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic() { companion object { val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")