From ad72f3e48f3967cc476384544749e7601b6270d2 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 3 Jun 2016 15:39:15 +0100 Subject: [PATCH] Add issuer to cash amounts Add issuer of a cash when referring to amounts of cash (except for the very few cases where the issuer is not important, such as when referring to aggregated totals across a set of issuers). Replaces CommonCashState with TokenDefinition, as a more accurate reflection of what the class represents. --- .../contracts/ICommercialPaperState.java | 3 +- .../contracts/JavaCommercialPaper.java | 17 ++- .../com/r3corda/contracts/CommercialPaper.kt | 10 +- .../contracts/cash/AssetIssuanceDefinition.kt | 15 --- .../kotlin/com/r3corda/contracts/cash/Cash.kt | 61 +++++---- .../r3corda/contracts/cash/FungibleAsset.kt | 21 ++- .../contracts/cash/FungibleAssetState.kt | 8 +- .../r3corda/contracts/testing/TestUtils.kt | 22 +++- .../protocols/TwoPartyTradeProtocol.kt | 8 +- .../r3corda/contracts/CommercialPaperTests.kt | 37 +++--- .../com/r3corda/contracts/cash/CashTests.kt | 120 +++++++++--------- .../r3corda/core/contracts/ContractsDSL.kt | 4 +- .../com/r3corda/core/contracts/Structures.kt | 11 ++ .../com/r3corda/core/serialization/Kryo.kt | 3 + .../-cash-issuance-definition/currency.html | 15 +++ .../-cash/generate-issue.html | 21 +++ .../com.r3corda.contracts/-cash/index.html | 104 +++++++++++++++ .../node/internal/testing/TradeSimulation.kt | 11 +- .../node/internal/testing/WalletFiller.kt | 3 +- .../node/services/wallet/NodeWalletService.kt | 1 - .../node/services/wallet/WalletImpl.kt | 4 +- .../messaging/TwoPartyTradeProtocolTests.kt | 57 +++++---- .../node/services/NodeInterestRatesTest.kt | 10 +- .../node/services/NodeWalletServiceTest.kt | 17 +-- .../kotlin/com/r3corda/demos/RateFixDemo.kt | 3 +- .../kotlin/com/r3corda/demos/TraderDemo.kt | 35 +++-- 26 files changed, 415 insertions(+), 206 deletions(-) delete mode 100644 contracts/src/main/kotlin/com/r3corda/contracts/cash/AssetIssuanceDefinition.kt create mode 100644 docs/build/html/api/com.r3corda.contracts.cash/-cash-issuance-definition/currency.html create mode 100644 docs/build/html/api/com.r3corda.contracts/-cash/generate-issue.html create mode 100644 docs/build/html/api/com.r3corda.contracts/-cash/index.html diff --git a/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java b/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java index aa40a2bd78..40ffce6315 100644 --- a/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java +++ b/contracts/src/main/java/com/r3corda/contracts/ICommercialPaperState.java @@ -3,6 +3,7 @@ package com.r3corda.contracts; import com.r3corda.core.contracts.Amount; import com.r3corda.core.contracts.ContractState; import com.r3corda.core.contracts.PartyAndReference; +import com.r3corda.core.contracts.Issued; import java.security.*; import java.time.*; @@ -18,7 +19,7 @@ public interface ICommercialPaperState extends ContractState { ICommercialPaperState withIssuance(PartyAndReference newIssuance); - ICommercialPaperState withFaceValue(Amount newFaceValue); + ICommercialPaperState withFaceValue(Amount> newFaceValue); ICommercialPaperState withMaturityDate(Instant newMaturityDate); } diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index f27919b99f..1d5609c91c 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -13,6 +13,7 @@ import org.jetbrains.annotations.Nullable; import java.security.PublicKey; import java.time.Instant; +import java.util.Currency; import java.util.List; import static com.r3corda.core.contracts.ContractsDSLKt.requireSingleCommand; @@ -30,14 +31,15 @@ public class JavaCommercialPaper implements Contract { public static class State implements ContractState, ICommercialPaperState { private PartyAndReference issuance; private PublicKey owner; - private Amount faceValue; + private Amount> faceValue; private Instant maturityDate; private Party notary; public State() { } // For serialization - public State(PartyAndReference issuance, PublicKey owner, Amount faceValue, Instant maturityDate, Party notary) { + public State(PartyAndReference issuance, PublicKey owner, Amount> faceValue, + Instant maturityDate, Party notary) { this.issuance = issuance; this.owner = owner; this.faceValue = faceValue; @@ -57,7 +59,7 @@ public class JavaCommercialPaper implements Contract { return new State(newIssuance, this.owner, this.faceValue, this.maturityDate, this.notary); } - public ICommercialPaperState withFaceValue(Amount newFaceValue) { + public ICommercialPaperState withFaceValue(Amount> newFaceValue) { return new State(this.issuance, this.owner, newFaceValue, this.maturityDate, this.notary); } @@ -73,7 +75,7 @@ public class JavaCommercialPaper implements Contract { return owner; } - public Amount getFaceValue() { + public Amount> getFaceValue() { return faceValue; } @@ -207,10 +209,11 @@ public class JavaCommercialPaper implements Contract { throw new IllegalArgumentException("Failed Requirement: must be timestamped"); Instant time = timestampCommand.getBefore(); - Amount received = CashKt.sumCashBy(tx.getOutStates(), input.getOwner()); + Amount> received = CashKt.sumCashBy(tx.getOutStates(), input.getOwner()); if (!received.equals(input.getFaceValue())) - throw new IllegalStateException("Failed Requirement: received amount equals the face value"); + throw new IllegalStateException("Failed Requirement: received amount equals the face value: " + + received + " vs " + input.getFaceValue()); if (time == null || time.isBefore(input.getMaturityDate())) throw new IllegalStateException("Failed requirement: the paper must have matured"); if (!input.getFaceValue().equals(received)) @@ -235,7 +238,7 @@ public class JavaCommercialPaper implements Contract { } public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> wallet) throws InsufficientBalanceException { - new Cash().generateSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet, null); + new Cash().generateSpend(tx, paper.getState().getFaceValue(), paper.getState().getOwner(), wallet); tx.addInputState(paper.getRef()); tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getOwner())); } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index 5250499827..8c670c8cd2 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -46,7 +46,7 @@ class CommercialPaper : Contract { data class State( val issuance: PartyAndReference, override val owner: PublicKey, - val faceValue: Amount, + val faceValue: Amount>, val maturityDate: Instant, override val notary: Party ) : OwnableState, ICommercialPaperState { @@ -60,7 +60,7 @@ class CommercialPaper : Contract { override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner) override fun withIssuance(newIssuance: PartyAndReference): ICommercialPaperState = copy(issuance = newIssuance) - override fun withFaceValue(newFaceValue: Amount): ICommercialPaperState = copy(faceValue = newFaceValue) + override fun withFaceValue(newFaceValue: Amount>): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) } @@ -136,7 +136,8 @@ class CommercialPaper : Contract { * an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction * at the moment: this restriction is not fundamental and may be lifted later. */ - fun generateIssue(issuance: PartyAndReference, faceValue: Amount, maturityDate: Instant, notary: Party): TransactionBuilder { + fun generateIssue(faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { + val issuance = faceValue.token.issuer val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate, notary) return TransactionBuilder().withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } @@ -160,7 +161,8 @@ class CommercialPaper : Contract { @Throws(InsufficientBalanceException::class) fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: List>) { // Add the cash movement using the states in our wallet. - Cash().generateSpend(tx, paper.state.faceValue, paper.state.owner, wallet) + val amount = paper.state.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } + Cash().generateSpend(tx, amount, paper.state.owner, wallet) tx.addInputState(paper.ref) tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.owner) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/AssetIssuanceDefinition.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/AssetIssuanceDefinition.kt deleted file mode 100644 index 906ba85a14..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/AssetIssuanceDefinition.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.r3corda.contracts.cash - -import com.r3corda.core.contracts.IssuanceDefinition -import com.r3corda.core.contracts.PartyAndReference -import java.util.* - -/** - * Subset of cash-like contract state, containing the issuance definition. If these definitions match for two - * contracts' states, those states can be aggregated. - */ -interface AssetIssuanceDefinition : IssuanceDefinition { - /** Where the underlying asset backing this ledger entry can be found (propagated) */ - val deposit: PartyAndReference - val token: T -} \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt index 7ccfee0f95..522fbbc522 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/Cash.kt @@ -44,28 +44,22 @@ class Cash : FungibleAsset() { */ override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - data class IssuanceDefinition( - /** Where the underlying currency backing this ledger entry can be found (propagated) */ - override val deposit: PartyAndReference, - - override val token: T - ) : AssetIssuanceDefinition - /** A state representing a cash claim against some party */ data class State( - /** Where the underlying currency backing this ledger entry can be found (propagated) */ - override val deposit: PartyAndReference, - - override val amount: Amount, + override val amount: Amount>, /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey, override val notary: Party ) : FungibleAsset.State { - override val issuanceDef: IssuanceDefinition - get() = IssuanceDefinition(deposit, amount.token) + constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey, notary: Party) + : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner, notary) + override val deposit: PartyAndReference + get() = amount.token.issuer override val contract = CASH_PROGRAM_ID + override val issuanceDef: Issued + get() = amount.token override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" @@ -86,25 +80,36 @@ class Cash : FungibleAsset() { * A command stating that money has been withdrawn from the shared ledger and is now accounted for * in some other way. */ - data class Exit(override val amount: Amount) : Commands, FungibleAsset.Commands.Exit + data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit } /** * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, issuanceDef: AssetIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party) - = generateIssue(tx, Amount(pennies, issuanceDef.token), issuanceDef.deposit, owner, notary) + fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: PublicKey, notary: Party) + = generateIssue(tx, Amount(pennies, tokenDef), owner, notary) /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyAndReference, owner: PublicKey, notary: Party) { + fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) { check(tx.inputStates().isEmpty()) check(tx.outputStates().sumCashOrNull() == null) - tx.addOutputState(Cash.State(at, amount, owner, notary)) + val at = amount.token.issuer + tx.addOutputState(Cash.State(amount, owner, notary)) 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. @@ -138,7 +143,7 @@ class Cash : FungibleAsset() { val currency = amount.token val acceptableCoins = run { - val ofCurrency = cashStates.filter { it.state.amount.token == currency } + val ofCurrency = cashStates.filter { it.state.amount.token.product == currency } if (onlyFromParties != null) ofCurrency.filter { it.state.deposit.party in onlyFromParties } else @@ -147,25 +152,31 @@ class Cash : FungibleAsset() { val gathered = arrayListOf>() var gatheredAmount = Amount(0, currency) + var takeChangeFrom: StateAndRef? = null for (c in acceptableCoins) { if (gatheredAmount >= amount) break gathered.add(c) - gatheredAmount += c.state.amount + gatheredAmount += Amount(c.state.amount.quantity, currency) + takeChangeFrom = c } if (gatheredAmount < amount) throw InsufficientBalanceException(amount - gatheredAmount) - val change = gatheredAmount - amount + val change = if (takeChangeFrom != null && gatheredAmount > amount) { + Amount>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.issuanceDef) + } else { + null + } val keysUsed = gathered.map { it.state.owner }.toSet() val states = gathered.groupBy { it.state.deposit }.map { val (deposit, coins) = it val totalAmount = coins.map { it.state.amount }.sumOrThrow() - State(deposit, totalAmount, to, coins.first().state.notary) + State(totalAmount, to, coins.first().state.notary) } - val outputs = if (change.quantity > 0) { + val outputs = if (change != null) { // Just copy a key across as the change key. In real life of course, this works but leaks private data. // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow // value flows through the transaction graph. @@ -173,7 +184,7 @@ class Cash : FungibleAsset() { // Add a change output and adjust the last output downwards. states.subList(0, states.lastIndex) + states.last().let { it.copy(amount = it.amount - change) } + - State(gathered.last().state.deposit, change, changeKey, gathered.last().state.notary) + State(change, changeKey, gathered.last().state.notary) } else states for (state in gathered) tx.addInputState(state.ref) @@ -204,4 +215,4 @@ fun Iterable.sumCash() = filterIsInstance().map { it. fun Iterable.sumCashOrNull() = 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: Currency) = filterIsInstance().map { it.amount }.sumOrZero(currency) \ No newline at end of file +fun Iterable.sumCashOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero>(currency) diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt index ac79efb7b2..417d16392e 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAsset.kt @@ -14,7 +14,7 @@ import java.util.* // Cash-like // -class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() +class InsufficientBalanceException(val amountMissing: Amount) : Exception() /** * Superclass for contracts representing assets which are fungible, countable and issued by a specific party. States @@ -30,11 +30,11 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception() * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.) */ abstract class FungibleAsset : Contract { - /** A state representing a claim against some party */ - interface State : FungibleAssetState> { - /** Where the underlying asset backing this ledger entry can be found (propagated) */ + /** A state representing a cash claim against some party */ + interface State : FungibleAssetState { + /** Where the underlying currency backing this ledger entry can be found (propagated) */ override val deposit: PartyAndReference - override val amount: Amount + override val amount: Amount> /** There must be a MoveCommand signed by this key to claim the amount */ override val owner: PublicKey override val notary: Party @@ -54,7 +54,7 @@ abstract class FungibleAsset : Contract { * A command stating that money has been withdrawn from the shared ledger and is now accounted for * in some other way. */ - interface Exit : Commands { val amount: Amount } + interface Exit : Commands { val amount: Amount> } } /** This is the function EVERYONE runs */ @@ -63,10 +63,9 @@ abstract class FungibleAsset : Contract { // and must be kept separated for bookkeeping purposes. val groups = tx.groupStates() { it: FungibleAsset.State -> it.issuanceDef } - for ((inputs, outputs, key) in groups) { + for ((inputs, outputs, token) in groups) { // Either inputs or outputs could be empty. - val deposit = key.deposit - val token = key.token + val deposit = token.issuer val issuer = deposit.party requireThat { @@ -100,7 +99,7 @@ abstract class FungibleAsset : Contract { outputs: List>, tx: TransactionForVerification, issueCommand: AuthenticatedObject, - token: T, + token: Issued, issuer: Party) { // If we have an issue command, perform special processing: the group is allowed to have no inputs, // and the output states must have a deposit reference owned by the signer. @@ -145,4 +144,4 @@ fun Iterable.sumFungible() = filterIsInstance Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() /** Sums the asset states in the list, returning zero of the given token if there are none. */ -fun Iterable.sumFungibleOrZero(token: T) = filterIsInstance>().map { it.amount }.sumOrZero(token) \ No newline at end of file +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt index 73ac45231a..e98cfdbe9e 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/cash/FungibleAssetState.kt @@ -3,14 +3,14 @@ package com.r3corda.contracts.cash import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.OwnableState import com.r3corda.core.contracts.PartyAndReference -import java.util.Currency +import com.r3corda.core.contracts.Issued /** * Common elements of cash contract states. */ -interface FungibleAssetState> : OwnableState { - val issuanceDef: I +interface FungibleAssetState : OwnableState { + val issuanceDef: Issued /** Where the underlying currency backing this ledger entry can be found (propagated) */ val deposit: PartyAndReference - val amount: Amount + val amount: Amount> } \ No newline at end of file diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index 692d25ceab..4b7a96532e 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -7,10 +7,12 @@ import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Contract import com.r3corda.core.contracts.DUMMY_PROGRAM_ID import com.r3corda.core.contracts.DummyContract +import com.r3corda.core.contracts.PartyAndReference +import com.r3corda.core.contracts.Issued import com.r3corda.core.crypto.NullPublicKey import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.testing.DUMMY_NOTARY -import com.r3corda.core.testing.MINI_CORP import java.security.PublicKey import java.util.* @@ -48,9 +50,21 @@ fun generateState(notary: Party = DUMMY_NOTARY) = DummyContract.State(Random().n // TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner) -infix fun Cash.State.`issued by`(party: Party) = copy(deposit = deposit.copy(party = party)) +infix fun Cash.State.`issued by`(party: Party) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit.copy(party = party)))) +infix fun Cash.State.`issued by`(deposit: PartyAndReference) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit))) + infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) -// Allows you to write 100.DOLLARS.CASH -val Amount.CASH: Cash.State get() = Cash.State(MINI_CORP.ref(1, 2, 3), this, NullPublicKey, DUMMY_NOTARY) +infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = + copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) + +val DUMMY_CASH_ISSUER_KEY = generateKeyPair() +val DUMMY_CASH_ISSUER = Party("Snake Oil Issuer", DUMMY_CASH_ISSUER_KEY.public).ref(1) +/** Allows you to write 100.DOLLARS.CASH */ +val Amount.CASH: Cash.State get() = Cash.State( + Amount>(this.quantity, Issued(DUMMY_CASH_ISSUER, this.token)), + NullPublicKey, DUMMY_NOTARY) + +val Amount>.STATE: Cash.State get() = Cash.State(this, NullPublicKey, DUMMY_NOTARY) + diff --git a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt index 4e70d68b03..21bafe4fb0 100644 --- a/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt +++ b/contracts/src/main/kotlin/com/r3corda/protocols/TwoPartyTradeProtocol.kt @@ -45,7 +45,7 @@ import java.util.Currency object TwoPartyTradeProtocol { val TRADE_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" } @@ -53,7 +53,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 ) @@ -64,7 +64,7 @@ object TwoPartyTradeProtocol { open class Seller(val otherSide: SingleMessageRecipient, 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() { @@ -174,7 +174,7 @@ object TwoPartyTradeProtocol { open class Buyer(val otherSide: SingleMessageRecipient, val notary: Party, - val acceptablePrice: Amount, + val acceptablePrice: Amount>, val typeToBuy: Class, val sessionID: Long) : ProtocolLogic() { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index c1a902b1a6..80d7a8814b 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -1,8 +1,10 @@ package com.r3corda.contracts import com.r3corda.contracts.testing.CASH +import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` import com.r3corda.contracts.cash.Cash +import com.r3corda.contracts.testing.STATE import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.days @@ -29,7 +31,7 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State( MEGA_CORP.ref(123), MEGA_CORP_PUBKEY, - 1000.DOLLARS, + 1000.DOLLARS `issued by` MEGA_CORP.ref(123), TEST_TX_TIME + 7.days, DUMMY_NOTARY ) @@ -43,7 +45,7 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = CommercialPaper.State( issuance = MEGA_CORP.ref(123), owner = MEGA_CORP_PUBKEY, - faceValue = 1000.DOLLARS, + faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), maturityDate = TEST_TX_TIME + 7.days, notary = DUMMY_NOTARY ) @@ -64,6 +66,7 @@ class CommercialPaperTestsGeneric { lateinit var thisTest: ICommercialPaperTestTemplate val attachments = MockStorageService().attachments + val issuer = MEGA_CORP.ref(123) @Test fun ok() { @@ -92,7 +95,7 @@ class CommercialPaperTestsGeneric { fun `face value is not zero`() { transactionGroup { transaction { - output { thisTest.getPaper().withFaceValue(0.DOLLARS) } + output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) } arg(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand() } timestamp(TEST_TX_TIME) } @@ -133,7 +136,7 @@ class CommercialPaperTestsGeneric { @Test fun `did not receive enough money at redemption`() { - trade(aliceGetsBack = 700.DOLLARS).expectFailureOfTx(3, "received amount equals the face value") + trade(aliceGetsBack = 700.DOLLARS `issued by` issuer).expectFailureOfTx(3, "received amount equals the face value") } @Test @@ -150,7 +153,7 @@ class CommercialPaperTestsGeneric { fun `issue move and then redeem`() { // MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val issueTX: LedgerTransaction = run { - val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + val ptx = CommercialPaper().generateIssue(10000.DOLLARS `issued by` MINI_CORP.ref(123), TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) @@ -160,9 +163,9 @@ class CommercialPaperTestsGeneric { } val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( - 3000.DOLLARS.CASH `owned by` ALICE_PUBKEY, - 3000.DOLLARS.CASH `owned by` ALICE_PUBKEY, - 3000.DOLLARS.CASH `owned by` ALICE_PUBKEY + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY, + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY, + 3000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` ALICE_PUBKEY ) // Alice pays $9000 to MiniCorp to own some of their debt. @@ -178,8 +181,8 @@ class CommercialPaperTestsGeneric { // Won't be validated. val (corpWalletTX, corpWallet) = cashOutputsToWallet( - 9000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY, - 4000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY + 9000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY, + 4000.DOLLARS.CASH `issued by` MINI_CORP.ref(123) `owned by` MINI_CORP_PUBKEY ) fun makeRedeemTX(time: Instant): LedgerTransaction { @@ -205,13 +208,13 @@ class CommercialPaperTestsGeneric { // Generate a trade lifecycle with various parameters. fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days, - aliceGetsBack: Amount = 1000.DOLLARS, + aliceGetsBack: Amount> = 1000.DOLLARS `issued by` issuer, destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL { - val someProfits = 1200.DOLLARS + val someProfits = 1200.DOLLARS `issued by` issuer return transactionGroupFor() { roots { - transaction(900.DOLLARS.CASH `owned by` ALICE_PUBKEY label "alice's $900") - transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits") + transaction(900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY label "alice's $900") + transaction(someProfits.STATE `owned by` MEGA_CORP_PUBKEY label "some profits") } // Some CP is issued onto the ledger by MegaCorp. @@ -226,7 +229,7 @@ class CommercialPaperTestsGeneric { transaction("Trade") { input("paper") input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY } + output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } output("alice's paper") { "paper".output `owned by` ALICE_PUBKEY } arg(ALICE_PUBKEY) { Cash.Commands.Move() } arg(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } @@ -238,8 +241,8 @@ class CommercialPaperTestsGeneric { input("alice's paper") input("some profits") - output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE_PUBKEY } - output("Change") { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY } + output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY } + output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY } if (!destroyPaperAtRedemption) output { "paper".output } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt index bd0ed46e2b..2b7ee70510 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTests.kt @@ -3,6 +3,7 @@ package com.r3corda.contracts.cash import com.r3corda.core.contracts.DummyContract import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` +import com.r3corda.contracts.testing.`with deposit` import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash @@ -18,15 +19,18 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class CashTests { + val defaultRef = OpaqueBytes(ByteArray(1, {1})) + val defaultIssuer = MEGA_CORP.ref(defaultRef) val inState = Cash.State( - deposit = MEGA_CORP.ref(1), - amount = 1000.DOLLARS, + amount = 1000.DOLLARS `issued by` defaultIssuer, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY ) val outState = inState.copy(owner = DUMMY_PUBKEY_2) - fun Cash.State.editDepositRef(ref: Byte) = copy(deposit = deposit.copy(reference = OpaqueBytes.of(ref))) + fun Cash.State.editDepositRef(ref: Byte) = copy( + amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref)))) + ) @Test fun trivial() { @@ -35,7 +39,7 @@ class CashTests { this `fails requirement` "the amounts balance" tweak { - output { outState.copy(amount = 2000.DOLLARS) } + output { outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer) } this `fails requirement` "the amounts balance" } tweak { @@ -84,9 +88,8 @@ class CashTests { transaction { output { Cash.State( - amount = 1000.DOLLARS, + amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, - deposit = MINI_CORP.ref(12, 34), notary = DUMMY_NOTARY ) } @@ -100,19 +103,19 @@ class CashTests { // Test generation works. val ptx = TransactionBuilder() - Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(ptx.inputStates().isEmpty()) val s = ptx.outputStates()[0] as Cash.State - assertEquals(100.DOLLARS, s.amount) + assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) assertEquals(MINI_CORP, s.deposit.party) assertEquals(DUMMY_PUBKEY_1, s.owner) assertTrue(ptx.commands()[0].value is Cash.Commands.Issue) assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0]) // Test issuance from the issuance definition - val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD) + val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) val templatePtx = TransactionBuilder() - Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.quantity, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) + Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) assertTrue(templatePtx.inputStates().isEmpty()) assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) @@ -180,14 +183,14 @@ class CashTests { // Issue some cash var ptx = TransactionBuilder() - Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) ptx.signWith(MINI_CORP_KEY) val tx = ptx.toSignedTransaction() // Include the previously issued cash in a new issuance command ptx = TransactionBuilder() ptx.addInputState(tx.tx.outRef(0).ref) - Cash().generateIssue(ptx, 100.DOLLARS, MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) } @Test @@ -221,13 +224,13 @@ class CashTests { fun zeroSizedValues() { transaction { input { inState } - input { inState.copy(amount = 0.DOLLARS) } + input { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) } this `fails requirement` "zero sized inputs" } transaction { input { inState } output { inState } - output { inState.copy(amount = 0.DOLLARS) } + output { inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer) } this `fails requirement` "zero sized outputs" } } @@ -250,19 +253,19 @@ class CashTests { // Can't mix currencies. transaction { input { inState } - output { outState.copy(amount = 800.DOLLARS) } - output { outState.copy(amount = 200.POUNDS) } + output { outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer) } + output { outState.copy(amount = 200.POUNDS `issued by` defaultIssuer) } this `fails requirement` "the amounts balance" } transaction { input { inState } input { inState.copy( - amount = 150.POUNDS, + amount = 150.POUNDS `issued by` defaultIssuer, owner = DUMMY_PUBKEY_2 ) } - output { outState.copy(amount = 1150.DOLLARS) } + output { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) } this `fails requirement` "the amounts balance" } // Can't have superfluous input states from different issuers. @@ -287,16 +290,16 @@ class CashTests { // Single input/output straightforward case. transaction { input { inState } - output { outState.copy(amount = inState.amount - 200.DOLLARS) } + output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) } tweak { - arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this `fails requirement` "the amounts balance" } tweak { - arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) } this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command" tweak { @@ -310,17 +313,17 @@ class CashTests { input { inState } input { inState `issued by` MINI_CORP } - output { inState.copy(amount = inState.amount - 200.DOLLARS) `issued by` MINI_CORP } - output { inState.copy(amount = inState.amount - 200.DOLLARS) } + output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP } + output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this `fails requirement` "at issuer MegaCorp the amounts balance" - arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } + arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) } this `fails requirement` "at issuer MiniCorp the amounts balance" - arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } + arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)) } this.accepts() } } @@ -334,7 +337,7 @@ class CashTests { // Can't merge them together. tweak { - output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS) } + output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS `issued by` defaultIssuer) } this `fails requirement` "at issuer MegaCorp the amounts balance" } // Missing MiniCorp deposit @@ -356,7 +359,7 @@ class CashTests { fun multiCurrency() { // Check we can do an atomic currency trade tx. transaction { - val pounds = Cash.State(MINI_CORP.ref(3, 4, 5), 658.POUNDS, DUMMY_PUBKEY_2, DUMMY_NOTARY) + val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), DUMMY_PUBKEY_2, DUMMY_NOTARY) input { inState `owned by` DUMMY_PUBKEY_1 } input { pounds } output { inState `owned by` DUMMY_PUBKEY_2 } @@ -376,7 +379,7 @@ class CashTests { fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = StateAndRef( - Cash.State(corp.ref(depositRef), amount, OUR_PUBKEY_1, DUMMY_NOTARY), + Cash.State(amount `issued by` corp.ref(depositRef), OUR_PUBKEY_1, DUMMY_NOTARY), StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) @@ -387,7 +390,7 @@ class CashTests { makeCash(80.SWISS_FRANCS, MINI_CORP, 2) ) - fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction { + fun makeSpend(amount: Amount, dest: PublicKey, corp: Party, depositRef: OpaqueBytes = defaultRef): WireTransaction { val tx = TransactionBuilder() Cash().generateSpend(tx, amount, dest, WALLET) return tx.toWireTransaction() @@ -395,7 +398,7 @@ class CashTests { @Test fun generateSimpleDirectSpend() { - val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0]) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) @@ -410,29 +413,30 @@ class CashTests { @Test fun generateSimpleSpendWithChange() { - val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS), wtx.outputs[0]) - assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS), wtx.outputs[1]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) + assertEquals(WALLET[0].state.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1]) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @Test fun generateSpendWithTwoInputs() { - val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[1].ref, wtx.inputs[1]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @Test fun generateSpendMixedDeposits() { - val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) + assertEquals(3, wtx.inputs.size) assertEquals(WALLET[0].ref, wtx.inputs[0]) assertEquals(WALLET[1].ref, wtx.inputs[1]) assertEquals(WALLET[2].ref, wtx.inputs[2]) - assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS), wtx.outputs[0]) + assertEquals(WALLET[0].state.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0]) assertEquals(WALLET[2].state.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1]) assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } @@ -440,12 +444,12 @@ class CashTests { @Test fun generateSpendInsufficientBalance() { val e: InsufficientBalanceException = assertFailsWith("balance") { - makeSpend(1000.DOLLARS, THEIR_PUBKEY_1) + makeSpend(1000.DOLLARS, THEIR_PUBKEY_1, MEGA_CORP) } assertEquals((1000 - 580).DOLLARS, e.amountMissing) assertFailsWith(InsufficientBalanceException::class) { - makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1) + makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1, MEGA_CORP) } } @@ -454,9 +458,9 @@ class CashTests { */ @Test fun aggregation() { - val fiveThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 5000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) - val twoThousandDollarsFromMega = Cash.State(MEGA_CORP.ref(2), 2000.DOLLARS, MINI_CORP_PUBKEY, DUMMY_NOTARY) - val oneThousandDollarsFromMini = Cash.State(MINI_CORP.ref(3), 1000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP_PUBKEY, DUMMY_NOTARY) + val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP_PUBKEY, DUMMY_NOTARY) + val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY, DUMMY_NOTARY) // Obviously it must be possible to aggregate states with themselves assertEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.issuanceDef) @@ -470,28 +474,28 @@ class CashTests { // States cannot be aggregated if the currency differs assertNotEquals(oneThousandDollarsFromMini.issuanceDef, - Cash.State(MINI_CORP.ref(3), 1000.POUNDS, MEGA_CORP_PUBKEY, DUMMY_NOTARY).issuanceDef) + Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY, DUMMY_NOTARY).issuanceDef) // States cannot be aggregated if the reference differs - assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef) - assertNotEquals(fiveThousandDollarsFromMega.copy(deposit = MEGA_CORP.ref(1)).issuanceDef, fiveThousandDollarsFromMega.issuanceDef) + assertNotEquals(fiveThousandDollarsFromMega.issuanceDef, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef) + assertNotEquals((fiveThousandDollarsFromMega `with deposit` defaultIssuer).issuanceDef, fiveThousandDollarsFromMega.issuanceDef) } @Test fun `summing by owner`() { val states = listOf( - Cash.State(MEGA_CORP.ref(1), 1000.DOLLARS, MINI_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(MEGA_CORP.ref(1), 2000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(MEGA_CORP.ref(1), 4000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) ) - assertEquals(6000.DOLLARS, states.sumCashBy(MEGA_CORP_PUBKEY)) + assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP_PUBKEY)) } @Test(expected = UnsupportedOperationException::class) fun `summing by owner throws`() { val states = listOf( - Cash.State(MEGA_CORP.ref(1), 2000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(MEGA_CORP.ref(1), 4000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) ) states.sumCashBy(MINI_CORP_PUBKEY) } @@ -499,7 +503,7 @@ class CashTests { @Test fun `summing no currencies`() { val states = emptyList() - assertEquals(0.POUNDS, states.sumCashOrZero(GBP)) + assertEquals(0.POUNDS `issued by` defaultIssuer, states.sumCashOrZero(GBP `issued by` defaultIssuer)) assertNull(states.sumCashOrNull()) } @@ -512,12 +516,12 @@ class CashTests { @Test fun `summing a single currency`() { val states = listOf( - Cash.State(MEGA_CORP.ref(1), 1000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(MEGA_CORP.ref(1), 2000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(MEGA_CORP.ref(1), 4000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) ) // Test that summing everything produces the total number of dollars - var expected = 7000.DOLLARS + var expected = 7000.DOLLARS `issued by` defaultIssuer var actual = states.sumCash() assertEquals(expected, actual) } @@ -525,8 +529,8 @@ class CashTests { @Test(expected = IllegalArgumentException::class) fun `summing multiple currencies`() { val states = listOf( - Cash.State(MEGA_CORP.ref(1), 1000.DOLLARS, MEGA_CORP_PUBKEY, DUMMY_NOTARY), - Cash.State(MEGA_CORP.ref(1), 4000.POUNDS, MEGA_CORP_PUBKEY, DUMMY_NOTARY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY), + Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP_PUBKEY, DUMMY_NOTARY) ) // Test that summing everything fails because we're mixing units states.sumCash() diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt index 479ccd2eee..75555e18b0 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -1,6 +1,5 @@ package com.r3corda.core.contracts -import com.r3corda.core.* import com.r3corda.core.crypto.Party import java.security.PublicKey import java.util.* @@ -30,6 +29,9 @@ val Int.SWISS_FRANCS: Amount get() = Amount(this.toLong() * 100, CHF) val Double.DOLLARS: Amount get() = Amount((this * 100).toLong(), USD) +infix fun Currency.`issued by`(deposit: PartyAndReference) : Issued = Issued(deposit, this) +infix fun Amount.`issued by`(deposit: PartyAndReference) : Amount> = Amount(quantity, token `issued by` deposit) + //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// class Requirements { 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 eefe181eeb..798e9616c4 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/Structures.kt @@ -41,6 +41,17 @@ interface ContractState { */ interface IssuanceDefinition +/** + * Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's + * quantifiable with integer quantities. + * + * @param P the type of product underlying the definition, for example [Currency]. + */ +data class Issued

( + val issuer: PartyAndReference, + val product: P +) + /** * A contract state that can have a single owner. */ diff --git a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt index 24a4df12ee..f1cc4da867 100644 --- a/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/com/r3corda/core/serialization/Kryo.kt @@ -292,6 +292,9 @@ fun createKryo(k: Kryo = Kryo()): Kryo { // This is required to make all the unit tests pass register(Party::class.java) + // Work around a bug in Kryo handling nested generics + register(Issued::class.java, ImmutableClassSerializer(Issued::class)) + noReferencesWithin() } } diff --git a/docs/build/html/api/com.r3corda.contracts.cash/-cash-issuance-definition/currency.html b/docs/build/html/api/com.r3corda.contracts.cash/-cash-issuance-definition/currency.html new file mode 100644 index 0000000000..42f4c17e93 --- /dev/null +++ b/docs/build/html/api/com.r3corda.contracts.cash/-cash-issuance-definition/currency.html @@ -0,0 +1,15 @@ + + +CashIssuanceDefinition.currency - + + + +com.r3corda.contracts.cash / CashIssuanceDefinition / currency
+
+

currency

+ +abstract val currency: Currency
+
+
+ + diff --git a/docs/build/html/api/com.r3corda.contracts/-cash/generate-issue.html b/docs/build/html/api/com.r3corda.contracts/-cash/generate-issue.html new file mode 100644 index 0000000000..ba04fdfa0f --- /dev/null +++ b/docs/build/html/api/com.r3corda.contracts/-cash/generate-issue.html @@ -0,0 +1,21 @@ + + +Cash.generateIssue - + + + +com.r3corda.contracts / Cash / generateIssue
+
+

generateIssue

+ +fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party): Unit
+

Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.

+
+
+ +fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyAndReference, owner: PublicKey, notary: Party): Unit
+

Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.

+
+
+ + diff --git a/docs/build/html/api/com.r3corda.contracts/-cash/index.html b/docs/build/html/api/com.r3corda.contracts/-cash/index.html new file mode 100644 index 0000000000..0170f4a74f --- /dev/null +++ b/docs/build/html/api/com.r3corda.contracts/-cash/index.html @@ -0,0 +1,104 @@ + + +Cash - + + + +com.r3corda.contracts / Cash
+
+

Cash

+class Cash : Contract
+

A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple +input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour +(a blend of issuer+depositRef) and you couldnt merge outputs of two colours together, but you COULD put them in +the same transaction.

+

The goal of this design is to ensure that money can be withdrawn from the ledger easily: if you receive some money +via this contract, you always know where to go in order to extract it from the R3 ledger, no matter how many hands +it has passed through in the intervening time.

+

At the same time, other contracts that just want money and dont care much who is currently holding it in their +vaults can ignore the issuer/depositRefs and just examine the amount fields.

+
+
+
+
+

Types

+ + + + + + + + + + + + + + + +
+Commands +interface Commands : CommandData
+IssuanceDefinition +data class IssuanceDefinition : CashIssuanceDefinition
+State +data class State : CommonCashState<IssuanceDefinition>

A state representing a cash claim against some party

+
+

Constructors

+ + + + + + + +
+<init> +Cash()

A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple +input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour +(a blend of issuer+depositRef) and you couldnt merge outputs of two colours together, but you COULD put them in +the same transaction.

+
+

Properties

+ + + + + + + +
+legalContractReference +val legalContractReference: SecureHash

TODO:

+
+

Functions

+ + + + + + + + + + + + + + + +
+generateIssue +fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party): Unit

Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.

+fun generateIssue(tx: TransactionBuilder, amount: Amount, at: PartyAndReference, owner: PublicKey, notary: Party): Unit

Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.

+
+generateSpend +fun generateSpend(tx: TransactionBuilder, amount: Amount, to: PublicKey, cashStates: List<StateAndRef<State>>, onlyFromParties: Set<Party>? = null): List<PublicKey>

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: its up to you to do that.

+
+verify +fun verify(tx: TransactionForVerification): Unit

This is the function EVERYONE runs

+
+ + 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 11e864b9f4..22f1a3231b 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 @@ -5,6 +5,9 @@ import com.google.common.util.concurrent.ListenableFuture import com.r3corda.contracts.CommercialPaper 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 @@ -29,7 +32,7 @@ class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwo WalletFiller.fillWithSomeTestCash(buyer.services, notary.info.identity, 1500.DOLLARS) val issuance = run { - val tx = CommercialPaper().generateIssue(seller.info.identity.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days, notary.info.identity) + val tx = CommercialPaper().generateIssue(1100.DOLLARS `issued by` seller.info.identity.ref(1, 2, 3), Instant.now() + 10.days, notary.info.identity) tx.setTime(Instant.now(), notary.info.identity, 30.seconds) tx.signWith(notary.storage.myLegalIdentityKey) tx.signWith(seller.storage.myLegalIdentityKey) @@ -37,11 +40,13 @@ class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwo } seller.services.storageService.validatedTransactions.addTransaction(issuance) + val cashIssuerKey = generateKeyPair() + val amount = 1000.DOLLARS `issued by` Party("Big friendly bank", cashIssuerKey.public).ref(1) val sessionID = random63BitValue() val buyerProtocol = TwoPartyTradeProtocol.Buyer(seller.net.myAddress, notary.info.identity, - 1000.DOLLARS, CommercialPaper.State::class.java, sessionID) + amount, CommercialPaper.State::class.java, sessionID) val sellerProtocol = TwoPartyTradeProtocol.Seller(buyer.net.myAddress, notary.info, - issuance.tx.outRef(0), 1000.DOLLARS, seller.storage.myLegalIdentityKey, sessionID) + issuance.tx.outRef(0), amount, seller.storage.myLegalIdentityKey, sessionID) linkConsensus(listOf(buyer, seller, notary), sellerProtocol) linkProtocolProgress(buyer, buyerProtocol) diff --git a/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt b/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt index 8d87bf0a16..538360bdfe 100644 --- a/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt +++ b/node/src/main/kotlin/com/r3corda/node/internal/testing/WalletFiller.kt @@ -2,6 +2,7 @@ package com.r3corda.node.internal.testing import com.r3corda.contracts.cash.Cash import com.r3corda.core.contracts.Amount +import com.r3corda.core.contracts.Issued import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.crypto.Party import com.r3corda.core.node.ServiceHub @@ -35,7 +36,7 @@ object WalletFiller { val issuance = TransactionBuilder() val freshKey = services.keyManagementService.freshKey() - cash.generateIssue(issuance, Amount(pennies, howMuch.token), depositRef, freshKey.public, notary) + cash.generateIssue(issuance, Amount(pennies, Issued(depositRef, howMuch.token)), freshKey.public, notary) issuance.signWith(myKey) return@map issuance.toSignedTransaction(true) diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt index 6c015fa373..b51d8a0f3f 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/NodeWalletService.kt @@ -5,7 +5,6 @@ import com.r3corda.core.contracts.* import com.r3corda.core.crypto.SecureHash import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.WalletService -import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.SingletonSerializeAsToken import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.trace diff --git a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt index 01e0667b3a..fe6fdd03a5 100644 --- a/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt +++ b/node/src/main/kotlin/com/r3corda/node/services/wallet/WalletImpl.kt @@ -26,7 +26,7 @@ class WalletImpl(override val states: List>) : Wallet // Select the states we own which are cash, ignore the rest, take the amounts. mapNotNull { (it.state as? Cash.State)?.amount }. // Turn into a Map> like { GBP -> (£100, £500, etc), USD -> ($2000, $50) } - groupBy { it.token }. + groupBy { it.token.product }. // Collapse to Map by summing all the amounts of the same currency together. - mapValues { it.value.sumOrThrow() } + mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() } } \ No newline at end of file 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 8324f71c05..2d45aa3030 100644 --- a/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt +++ b/node/src/test/kotlin/com/r3corda/node/messaging/TwoPartyTradeProtocolTests.kt @@ -57,14 +57,14 @@ class TwoPartyTradeProtocolTests { lateinit var net: MockNetwork private fun runSeller(smm: StateMachineManager, notary: NodeInfo, - otherSide: SingleMessageRecipient, assetToSell: StateAndRef, price: Amount, + otherSide: SingleMessageRecipient, assetToSell: StateAndRef, price: Amount>, myKeyPair: KeyPair, buyerSessionID: Long): ListenableFuture { val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, assetToSell, price, myKeyPair, buyerSessionID) return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.seller", seller) } private fun runBuyer(smm: StateMachineManager, notaryNode: NodeInfo, - otherSide: SingleMessageRecipient, acceptablePrice: Amount, typeToBuy: Class, + otherSide: SingleMessageRecipient, acceptablePrice: Amount>, typeToBuy: Class, sessionID: Long): ListenableFuture { val buyer = TwoPartyTradeProtocol.Buyer(otherSide, notaryNode.identity, acceptablePrice, typeToBuy, sessionID) return smm.add("${TwoPartyTradeProtocol.TRADE_TOPIC}.buyer", buyer) @@ -94,8 +94,9 @@ class TwoPartyTradeProtocolTests { val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY) WalletFiller.fillWithSomeTestCash(bobNode.services, DUMMY_NOTARY, 2000.DOLLARS) + val issuer = bobNode.services.storageService.myLegalIdentity.ref(0) val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey, - notaryNode.info.identity, null).second + 1200.DOLLARS `issued by` issuer, notaryNode.info.identity, null).second insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey, notaryNode.storage.myLegalIdentityKey) @@ -106,7 +107,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobNode.net.myAddress, lookup("alice's paper"), - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID ) @@ -114,7 +115,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceNode.net.myAddress, - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, CommercialPaper.State::class.java, buyerSessionID ) @@ -141,12 +142,13 @@ class TwoPartyTradeProtocolTests { val aliceAddr = aliceNode.net.myAddress val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle val networkMapAddr = notaryNode.info + val issuer = bobNode.services.storageService.myLegalIdentity.ref(0) net.runNetwork() // Clear network map registration messages WalletFiller.fillWithSomeTestCash(bobNode.services, DUMMY_NOTARY, 2000.DOLLARS) val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey, - notaryNode.info.identity, null).second + 1200.DOLLARS `issued by` issuer, notaryNode.info.identity, null).second insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey) val buyerSessionID = random63BitValue() @@ -156,7 +158,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobAddr, lookup("alice's paper"), - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID ) @@ -164,7 +166,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceAddr, - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, CommercialPaper.State::class.java, buyerSessionID ) @@ -260,10 +262,11 @@ class TwoPartyTradeProtocolTests { } val attachmentID = aliceNode.storage.attachments.importAttachment(ByteArrayInputStream(stream.toByteArray())) - val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second + val issuer = MEGA_CORP.ref(1) + val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, issuer).second val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services) val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey, - notaryNode.info.identity, attachmentID).second + 1200.DOLLARS `issued by` issuer, notaryNode.info.identity, attachmentID).second val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey) val buyerSessionID = random63BitValue() @@ -275,7 +278,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobNode.net.myAddress, lookup("alice's paper"), - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID ) @@ -283,7 +286,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceNode.net.myAddress, - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, CommercialPaper.State::class.java, buyerSessionID ) @@ -366,13 +369,15 @@ class TwoPartyTradeProtocolTests { val notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) val aliceNode = net.createPartyNode(notaryNode.info, ALICE.name, ALICE_KEY) val bobNode = net.createPartyNode(notaryNode.info, BOB.name, BOB_KEY) + val issuer = MEGA_CORP.ref(1, 2, 3) val aliceAddr = aliceNode.net.myAddress val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle val bobKey = bobNode.keyManagement.freshKey() val bobsBadCash = fillUpForBuyer(bobError, bobKey.public).second - val alicesFakePaper = fillUpForSeller(aliceError, aliceNode.storage.myLegalIdentity.owningKey, notaryNode.info.identity, null).second + val alicesFakePaper = fillUpForSeller(aliceError, aliceNode.storage.myLegalIdentity.owningKey, + 1200.DOLLARS `issued by` issuer, notaryNode.info.identity, null).second insertFakeTransactions(bobsBadCash, bobNode.services, bobNode.storage.myLegalIdentityKey, bobNode.storage.myLegalIdentityKey) insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey) @@ -386,7 +391,7 @@ class TwoPartyTradeProtocolTests { notaryNode.info, bobAddr, lookup("alice's paper"), - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, ALICE_KEY, buyerSessionID ) @@ -394,7 +399,7 @@ class TwoPartyTradeProtocolTests { bobNode.smm, notaryNode.info, aliceAddr, - 1000.DOLLARS, + 1000.DOLLARS `issued by` issuer, CommercialPaper.State::class.java, buyerSessionID ) @@ -423,14 +428,16 @@ class TwoPartyTradeProtocolTests { return signed.associateBy { it.id } } - private fun TransactionGroupDSL.fillUpForBuyer(withError: Boolean, owner: PublicKey = BOB_PUBKEY): Pair> { + private fun TransactionGroupDSL.fillUpForBuyer(withError: Boolean, + owner: PublicKey = BOB_PUBKEY, + issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair> { // Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she // wants to sell to Bob. val eb1 = transaction { // Issued money to itself. - output("elbonian money 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY } - output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY } + output("elbonian money 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } + output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } if (!withError) arg(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() } timestamp(TEST_TX_TIME) @@ -439,14 +446,14 @@ class TwoPartyTradeProtocolTests { // Bob gets some cash onto the ledger from BoE val bc1 = transaction { input("elbonian money 1") - output("bob cash 1") { 800.DOLLARS.CASH `issued by` MEGA_CORP `owned by` owner } + output("bob cash 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` owner } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } } val bc2 = transaction { input("elbonian money 2") - output("bob cash 2") { 300.DOLLARS.CASH `issued by` MEGA_CORP `owned by` owner } - output { 700.DOLLARS.CASH `issued by` MEGA_CORP `owned by` MEGA_CORP_PUBKEY } // Change output. + output("bob cash 2") { 300.DOLLARS.CASH `issued by` issuer `owned by` owner } + output { 700.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } // Change output. arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } } @@ -454,10 +461,14 @@ class TwoPartyTradeProtocolTests { return Pair(wallet, listOf(eb1, bc1, bc2)) } - private fun TransactionGroupDSL.fillUpForSeller(withError: Boolean, owner: PublicKey, notary: Party, attachmentID: SecureHash?): Pair> { + private fun TransactionGroupDSL.fillUpForSeller(withError: Boolean, + owner: PublicKey, + amount: Amount>, + notary: Party, + attachmentID: SecureHash?): Pair> { val ap = transaction { output("alice's paper") { - CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, 1200.DOLLARS, TEST_TX_TIME + 7.days, notary) + CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days, notary) } arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } if (!withError) diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt index 52eabed3eb..0ce88da61e 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeInterestRatesTest.kt @@ -2,20 +2,23 @@ package com.r3corda.node.services import com.r3corda.contracts.cash.Cash import com.r3corda.contracts.testing.CASH +import com.r3corda.contracts.testing.`issued by` import com.r3corda.contracts.testing.`owned by` import com.r3corda.core.bd import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.Fix import com.r3corda.core.contracts.TransactionBuilder +import com.r3corda.core.crypto.Party +import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.testing.ALICE_PUBKEY import com.r3corda.core.testing.MEGA_CORP import com.r3corda.core.testing.MEGA_CORP_KEY import com.r3corda.core.utilities.BriefLogFormatter import com.r3corda.node.internal.testing.MockNetwork import com.r3corda.node.services.clientapi.NodeInterestRates +import com.r3corda.protocols.RatesFixProtocol import org.junit.Assert import org.junit.Test -import com.r3corda.protocols.RatesFixProtocol import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -29,6 +32,9 @@ class NodeInterestRatesTest { EURIBOR 2016-03-15 2M = 0.111 """.trimIndent()) + val DUMMY_CASH_ISSUER_KEY = generateKeyPair() + val DUMMY_CASH_ISSUER = Party("Cash issuer", DUMMY_CASH_ISSUER_KEY.public) + val oracle = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY).apply { knownFixes = TEST_DATA } @Test fun `query successfully`() { @@ -111,5 +117,5 @@ class NodeInterestRatesTest { assertEquals("0.678".bd, fix.value) } - private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `owned by` ALICE_PUBKEY)) + private fun makeTX() = TransactionBuilder(outputs = mutableListOf(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY)) } \ No newline at end of file diff --git a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt index c7d870f46f..129d42ac5d 100644 --- a/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt +++ b/node/src/test/kotlin/com/r3corda/node/services/NodeWalletServiceTest.kt @@ -1,6 +1,7 @@ package com.r3corda.node.services import com.r3corda.contracts.cash.Cash +import com.r3corda.core.contracts.`issued by` import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.contracts.USD @@ -51,13 +52,13 @@ class NodeWalletServiceTest { assertEquals(3, w.states.size) val state = w.states[0].state as Cash.State - assertEquals(services.storageService.myLegalIdentity, state.deposit.party) - assertEquals(services.storageService.myLegalIdentityKey.public, state.deposit.party.owningKey) - assertEquals(29.01.DOLLARS, state.amount) + val myIdentity = services.storageService.myLegalIdentity + val myPartyRef = myIdentity.ref(ref) + assertEquals(29.01.DOLLARS `issued by` myPartyRef, state.amount) assertEquals(ALICE_PUBKEY, state.owner) - assertEquals(33.34.DOLLARS, (w.states[2].state as Cash.State).amount) - assertEquals(35.61.DOLLARS, (w.states[1].state as Cash.State).amount) + assertEquals(33.34.DOLLARS `issued by` myPartyRef, (w.states[2].state as Cash.State).amount) + assertEquals(35.61.DOLLARS `issued by` myPartyRef, (w.states[1].state as Cash.State).amount) } @Test @@ -67,20 +68,20 @@ class NodeWalletServiceTest { // A tx that sends us money. val freshKey = services.keyManagementService.freshKey() val usefulTX = TransactionBuilder().apply { - Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() val myOutput = usefulTX.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments).outRef(0) // A tx that spends our money. val spendTX = TransactionBuilder().apply { - Cash().generateSpend(this, 80.DOLLARS, BOB_PUBKEY, listOf(myOutput)) + Cash().generateSpend(this, 80.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_PUBKEY, listOf(myOutput)) signWith(freshKey) }.toSignedTransaction() // A tx that doesn't send us anything. val irrelevantTX = TransactionBuilder().apply { - Cash().generateIssue(this, 100.DOLLARS, MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() diff --git a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt index 74d57a34c4..41fb88f3f9 100644 --- a/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/RateFixDemo.kt @@ -3,6 +3,7 @@ package com.r3corda.demos import com.r3corda.contracts.cash.Cash import com.r3corda.core.contracts.DOLLARS import com.r3corda.core.contracts.FixOf +import com.r3corda.core.contracts.`issued by` import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.crypto.Party import com.r3corda.core.logElapsedTime @@ -86,7 +87,7 @@ fun main(args: Array) { // Make a garbage transaction that includes a rate fix. val tx = TransactionBuilder() - tx.addOutputState(Cash.State(node.storage.myLegalIdentity.ref(1), 1500.DOLLARS, node.keyManagement.freshKey().public, notary.identity)) + tx.addOutputState(Cash.State(1500.DOLLARS `issued by` node.storage.myLegalIdentity.ref(1), node.keyManagement.freshKey().public, notary.identity)) val protocol = RatesFixProtocol(tx, oracleNode, fixOf, expectedRate, rateTolerance) node.smm.add("demo.ratefix", protocol).get() node.stop() diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index 107ff7ed1e..fb68cee333 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -16,9 +16,6 @@ import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.random63BitValue import com.r3corda.core.seconds import com.r3corda.core.serialization.deserialize -import com.r3corda.core.utilities.BriefLogFormatter -import com.r3corda.core.utilities.Emoji -import com.r3corda.core.utilities.ProgressTracker import com.r3corda.node.internal.Node import com.r3corda.node.internal.testing.WalletFiller import com.r3corda.node.services.config.NodeConfigurationFromConfig @@ -26,6 +23,9 @@ import com.r3corda.node.services.messaging.ArtemisMessagingService import com.r3corda.node.services.network.NetworkMapService import com.r3corda.node.services.persistence.NodeAttachmentService import com.r3corda.node.services.transactions.SimpleNotaryService +import com.r3corda.core.utilities.BriefLogFormatter +import com.r3corda.core.utilities.Emoji +import com.r3corda.core.utilities.ProgressTracker import com.r3corda.protocols.NotaryProtocol import com.r3corda.protocols.TwoPartyTradeProtocol import com.typesafe.config.ConfigFactory @@ -36,6 +36,7 @@ import java.nio.file.Path import java.nio.file.Paths import java.security.PublicKey import java.time.Instant +import java.util.* import kotlin.system.exitProcess import kotlin.test.assertEquals @@ -66,6 +67,9 @@ enum class Role { val DIRNAME = "trader-demo" fun main(args: Array) { + val cashIssuerKey = generateKeyPair() + val cashIssuer = Party("Trusted cash issuer", cashIssuerKey.public) + val amount = 1000.DOLLARS `issued by` cashIssuer.ref(1) val parser = OptionParser() val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required() @@ -134,9 +138,9 @@ fun main(args: Array) { // 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. if (role == Role.BUYER) { - runBuyer(node) + runBuyer(node, amount) } else { - runSeller(myNetAddr, node, theirNetAddr) + runSeller(myNetAddr, node, theirNetAddr, amount) } } @@ -156,7 +160,7 @@ fun parseOptions(args: Array, parser: OptionParser): OptionSet { } } -fun runSeller(myNetAddr: HostAndPort, node: Node, theirNetAddr: HostAndPort) { +fun runSeller(myNetAddr: HostAndPort, node: Node, theirNetAddr: HostAndPort, amount: Amount>) { // 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 @@ -176,14 +180,14 @@ fun runSeller(myNetAddr: HostAndPort, node: Node, theirNetAddr: HostAndPort) { } } else { val otherSide = ArtemisMessagingService.makeRecipient(theirNetAddr) - val seller = TraderDemoProtocolSeller(myNetAddr, otherSide) + val seller = TraderDemoProtocolSeller(myNetAddr, otherSide, amount) node.smm.add("demo.seller", seller).get() } node.stop() } -fun runBuyer(node: Node) { +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 { @@ -196,7 +200,7 @@ fun runBuyer(node: Node) { future } else { // We use a simple scenario-specific wrapper protocol to make things happen. - val buyer = TraderDemoProtocolBuyer(attachmentsPath, node.info.identity) + val buyer = TraderDemoProtocolBuyer(attachmentsPath, node.info.identity, amount) node.smm.add("demo.buyer", buyer) } @@ -207,7 +211,9 @@ fun runBuyer(node: Node) { val DEMO_TOPIC = "initiate.demo.trade" -class TraderDemoProtocolBuyer(private val attachmentsPath: Path, val notary: Party) : ProtocolLogic() { +class TraderDemoProtocolBuyer(private val attachmentsPath: Path, + val notary: Party, + val amount: Amount>) : ProtocolLogic() { companion object { object WAITING_FOR_SELLER_TO_CONNECT : ProgressTracker.Step("Waiting for seller to connect to us") @@ -240,7 +246,7 @@ class TraderDemoProtocolBuyer(private val attachmentsPath: Path, val notary: Par send(DEMO_TOPIC, newPartnerAddr, 0, sessionID) val notary = serviceHub.networkMapCache.notaryNodes[0] - val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, notary.identity, 1000.DOLLARS, + val buyer = TwoPartyTradeProtocol.Buyer(newPartnerAddr, notary.identity, amount, CommercialPaper.State::class.java, sessionID) // This invokes the trading protocol and out pops our finished transaction. @@ -284,6 +290,7 @@ ${Emoji.renderIfSupported(cpIssuance)}""") class TraderDemoProtocolSeller(val myAddress: HostAndPort, val otherSide: SingleMessageRecipient, + val amount: Amount>, override val progressTracker: ProgressTracker = TraderDemoProtocolSeller.tracker()) : ProtocolLogic() { companion object { val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9") @@ -316,7 +323,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort, progressTracker.currentStep = TRADING - val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, commercialPaper, 1000.DOLLARS, cpOwnerKey, + val seller = TwoPartyTradeProtocol.Seller(otherSide, notary, commercialPaper, amount, cpOwnerKey, sessionID, progressTracker.getChildProgressTracker(TRADING)!!) val tradeTX: SignedTransaction = subProtocol(seller) serviceHub.recordTransactions(listOf(tradeTX)) @@ -331,7 +338,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort, val party = Party("Bank of London", keyPair.public) val issuance: SignedTransaction = run { - val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS, Instant.now() + 10.days, notaryNode.identity) + val tx = CommercialPaper().generateIssue(1100.DOLLARS `issued by` party.ref(1, 2, 3), Instant.now() + 10.days, notaryNode.identity) // TODO: Consider moving these two steps below into generateIssue. @@ -369,4 +376,4 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort, return move.tx.outRef(0) } -} +} \ No newline at end of file