Merged in rnicoll-non-currency-cash (pull request #124)

Genericise Cash contract to support non-Currency things
This commit is contained in:
Ross Nicoll 2016-06-07 13:49:53 +01:00
commit ba1a6ab614
17 changed files with 221 additions and 140 deletions

View File

@ -168,7 +168,7 @@ public class JavaCommercialPaper implements Contract {
if (!inputs.isEmpty()) { if (!inputs.isEmpty()) {
throw new IllegalStateException("Failed Requirement: there is no input state"); throw new IllegalStateException("Failed Requirement: there is no input state");
} }
if (output.faceValue.getPennies() == 0) { if (output.faceValue.getQuantity() == 0) {
throw new IllegalStateException("Failed Requirement: the face value is not zero"); throw new IllegalStateException("Failed Requirement: the face value is not zero");
} }

View File

@ -117,7 +117,7 @@ class CommercialPaper : Contract {
// Don't allow people to issue commercial paper under other entities identities. // Don't allow people to issue commercial paper under other entities identities.
"the issuance is signed by the claimed issuer of the paper" by "the issuance is signed by the claimed issuer of the paper" by
(output.issuance.party.owningKey in command.signers) (output.issuance.party.owningKey in command.signers)
"the face value is not zero" by (output.faceValue.pennies > 0) "the face value is not zero" by (output.faceValue.quantity > 0)
"the maturity date is not in the past" by (time < output.maturityDate) "the maturity date is not in the past" by (time < output.maturityDate)
// Don't allow an existing CP state to be replaced by this issuance. // Don't allow an existing CP state to be replaced by this issuance.
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch. // TODO: Consider how to handle the case of mistaken issuances, or other need to patch.

View File

@ -88,7 +88,7 @@ class CrowdFund : Contract {
"there is no input state" by tx.inStates.filterIsInstance<State>().isEmpty() "there is no input state" by tx.inStates.filterIsInstance<State>().isEmpty()
"the transaction is signed by the owner of the crowdsourcing" by (command.signers.contains(outputCrowdFund.campaign.owner)) "the transaction is signed by the owner of the crowdsourcing" by (command.signers.contains(outputCrowdFund.campaign.owner))
"the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty()) "the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty())
"the output registration has a non-zero target" by (outputCrowdFund.campaign.target.pennies > 0) "the output registration has a non-zero target" by (outputCrowdFund.campaign.target.quantity > 0)
"the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank()) "the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank())
"the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime) "the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime)
"the output registration has an open state" by (!outputCrowdFund.closed) "the output registration has an open state" by (!outputCrowdFund.closed)

View File

@ -112,7 +112,7 @@ class FixedRatePaymentEvent(date: LocalDate,
} }
override val flow: Amount<Currency> get() = override val flow: Amount<Currency> get() =
Amount<Currency>(dayCountFactor.times(BigDecimal(notional.pennies)).times(rate.ratioUnit!!.value).toLong(), notional.token) Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token)
override fun toString(): String = override fun toString(): String =
"FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow" "FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow"
@ -138,7 +138,7 @@ class FloatingRatePaymentEvent(date: LocalDate,
override val flow: Amount<Currency> get() { override val flow: Amount<Currency> get() {
// TODO: Should an uncalculated amount return a zero ? null ? etc. // TODO: Should an uncalculated amount return a zero ? null ? etc.
val v = rate.ratioUnit?.value ?: return Amount<Currency>(0, notional.token) val v = rate.ratioUnit?.value ?: return Amount<Currency>(0, notional.token)
return Amount<Currency>(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.token) return Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token)
} }
override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow" override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow"
@ -456,7 +456,7 @@ class InterestRateSwap() : Contract {
fun checkLegAmounts(legs: Array<CommonLeg>) { fun checkLegAmounts(legs: Array<CommonLeg>) {
requireThat { requireThat {
"The notional is non zero" by legs.any { it.notional.pennies > (0).toLong() } "The notional is non zero" by legs.any { it.notional.quantity > (0).toLong() }
"The notional for all legs must be the same" by legs.all { it.notional == legs[0].notional } "The notional for all legs must be the same" by legs.all { it.notional == legs[0].notional }
} }
for (leg: CommonLeg in legs) { for (leg: CommonLeg in legs) {
@ -505,7 +505,7 @@ class InterestRateSwap() : Contract {
"There are no in states for an agreement" by inputs.isEmpty() "There are no in states for an agreement" by inputs.isEmpty()
"There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0) "There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0)
"There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0) "There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0)
"All notionals must be non zero" by (irs.fixedLeg.notional.pennies > 0 && irs.floatingLeg.notional.pennies > 0) "All notionals must be non zero" by (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0)
"The fixed leg rate must be positive" by (irs.fixedLeg.fixedRate.isPositive()) "The fixed leg rate must be positive" by (irs.fixedLeg.fixedRate.isPositive())
"The currency of the notionals must be the same" by (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token) "The currency of the notionals must be the same" by (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token)
"All leg notionals must be the same" by (irs.fixedLeg.notional == irs.floatingLeg.notional) "All leg notionals must be the same" by (irs.fixedLeg.notional == irs.floatingLeg.notional)

View File

@ -95,7 +95,7 @@ class ReferenceRate(val oracle: String, val tenor: Tenor, val name: String) : Fl
} }
// TODO: For further discussion. // TODO: For further discussion.
operator fun Amount<Currency>.times(other: RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.token) operator fun Amount<Currency>.times(other: RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.quantity).multiply(other.value)).longValueExact(), this.token)
//operator fun Amount<Currency>.times(other: FixedRate): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) //operator fun Amount<Currency>.times(other: FixedRate): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)
//fun Amount<Currency>.times(other: InterestRateSwap.RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) //fun Amount<Currency>.times(other: InterestRateSwap.RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)

View File

@ -8,8 +8,8 @@ import java.util.*
* Subset of cash-like contract state, containing the issuance definition. If these definitions match for two * Subset of cash-like contract state, containing the issuance definition. If these definitions match for two
* contracts' states, those states can be aggregated. * contracts' states, those states can be aggregated.
*/ */
interface CashIssuanceDefinition : IssuanceDefinition { interface AssetIssuanceDefinition<T> : IssuanceDefinition {
/** Where the underlying currency backing this ledger entry can be found (propagated) */ /** Where the underlying asset backing this ledger entry can be found (propagated) */
val deposit: PartyAndReference val deposit: PartyAndReference
val currency: Currency val token: T
} }

View File

@ -18,8 +18,6 @@ import java.util.*
val CASH_PROGRAM_ID = Cash() val CASH_PROGRAM_ID = Cash()
//SecureHash.sha256("cash") //SecureHash.sha256("cash")
class InsufficientBalanceException(val amountMissing: Amount<Currency>) : Exception()
/** /**
* A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * 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 * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour
@ -33,7 +31,7 @@ class InsufficientBalanceException(val amountMissing: Amount<Currency>) : Except
* At the same time, other contracts that just want money and don't care much who is currently holding it in their * At the same time, other contracts that just want money and don't care much who is currently holding it in their
* vaults can ignore the issuer/depositRefs and just examine the amount fields. * vaults can ignore the issuer/depositRefs and just examine the amount fields.
*/ */
class Cash : Contract { class Cash : FungibleAsset<Currency>() {
/** /**
* TODO: * TODO:
* 1) hash should be of the contents, not the URI * 1) hash should be of the contents, not the URI
@ -46,12 +44,12 @@ class Cash : Contract {
*/ */
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
data class IssuanceDefinition( data class IssuanceDefinition<T>(
/** Where the underlying currency backing this ledger entry can be found (propagated) */ /** Where the underlying currency backing this ledger entry can be found (propagated) */
override val deposit: PartyAndReference, override val deposit: PartyAndReference,
override val currency: Currency override val token: T
) : CashIssuanceDefinition ) : AssetIssuanceDefinition<T>
/** A state representing a cash claim against some party */ /** A state representing a cash claim against some party */
data class State( data class State(
@ -64,9 +62,9 @@ class Cash : Contract {
override val owner: PublicKey, override val owner: PublicKey,
override val notary: Party override val notary: Party
) : CommonCashState<Cash.IssuanceDefinition> { ) : FungibleAsset.State<Currency> {
override val issuanceDef: Cash.IssuanceDefinition override val issuanceDef: IssuanceDefinition<Currency>
get() = Cash.IssuanceDefinition(deposit, amount.token) get() = IssuanceDefinition(deposit, amount.token)
override val contract = CASH_PROGRAM_ID override val contract = CASH_PROGRAM_ID
override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})"
@ -76,93 +74,26 @@ class Cash : Contract {
// Just for grouping // Just for grouping
interface Commands : CommandData { interface Commands : CommandData {
class Move() : TypeOnlyCommandData(), Commands class Move() : TypeOnlyCommandData(), FungibleAsset.Commands.Move
/** /**
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
* has a unique ID even when there are no inputs. * has a unique ID even when there are no inputs.
*/ */
data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands data class Issue(override val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : FungibleAsset.Commands.Issue
/** /**
* A command stating that money has been withdrawn from the shared ledger and is now accounted for * A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way. * in some other way.
*/ */
data class Exit(val amount: Amount<Currency>) : Commands data class Exit(override val amount: Amount<Currency>) : Commands, FungibleAsset.Commands.Exit<Currency>
}
/** This is the function EVERYONE runs */
override fun verify(tx: TransactionForVerification) {
// Each group is a set of input/output states with distinct (deposit, currency) attributes. These types
// of cash are not fungible and must be kept separated for bookkeeping purposes.
val groups = tx.groupStates() { it: Cash.State -> it.issuanceDef }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val deposit = key.deposit
val currency = key.currency
val issuer = deposit.party
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L }
}
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
} else {
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group")
val outputAmount = outputs.sumCashOrZero(currency)
// If we want to remove cash from the ledger, that must be signed for by the issuer.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
val exitCommand = tx.commands.select<Commands.Exit>(party = issuer).singleOrNull()
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency)
requireThat {
"there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L }
"for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommands<Commands.Move>(inputs, tx)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: TransactionForVerification,
issueCommand: AuthenticatedObject<Commands.Issue>,
currency: Currency,
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.
//
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
// sum to more than the inputs. An issuance of zero size is not allowed.
//
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
// as-yet-unwritten identity service. See ADP-22 for discussion.
// The grouping ensures that all outputs have the same deposit reference and currency.
val inputAmount = inputs.sumCashOrZero(currency)
val outputAmount = outputs.sumCash()
val cashCommands = tx.commands.select<Cash.Commands>()
requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
"output deposits are owned by a command signer" by (issuer in issueCommand.signingParties)
"output values sum to more than the inputs" by (outputAmount > inputAmount)
"there is only a single issue command" by (cashCommands.count() == 1)
}
} }
/** /**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
*/ */
fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party) fun generateIssue(tx: TransactionBuilder, issuanceDef: AssetIssuanceDefinition<Currency>, pennies: Long, owner: PublicKey, notary: Party)
= generateIssue(tx, Amount(pennies, issuanceDef.currency), issuanceDef.deposit, owner, notary) = generateIssue(tx, Amount(pennies, issuanceDef.token), issuanceDef.deposit, owner, notary)
/** /**
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
@ -234,7 +165,7 @@ class Cash : Contract {
State(deposit, totalAmount, to, coins.first().state.notary) State(deposit, totalAmount, to, coins.first().state.notary)
} }
val outputs = if (change.pennies > 0) { val outputs = if (change.quantity > 0) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data. // 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 // 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. // value flows through the transaction graph.

View File

@ -0,0 +1,148 @@
package com.r3corda.contracts.cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.utilities.Emoji
import java.security.PublicKey
import java.security.SecureRandom
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Cash-like
//
class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception()
/**
* Superclass for contracts representing assets which are fungible, countable and issued by a specific party. States
* contain assets which are equivalent (such as cash of the same currency), so records of their existence can
* be merged or split as needed where the issuer is the same. For instance, dollars issued by the Fed are fungible and
* countable (in cents), barrels of West Texas crude are fungible and countable (oil from two small containers
* can be poured into one large container), shares of the same class in a specific company are fungible and
* countable, and so on.
*
* See [Cash] for an example subclass that implements currency.
*
* @param T a type that represents the asset in question. This should describe the basic type of the asset
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.)
*/
abstract class FungibleAsset<T> : Contract {
/** A state representing a claim against some party */
interface State<T> : FungibleAssetState<T, AssetIssuanceDefinition<T>> {
/** Where the underlying asset backing this ledger entry can be found (propagated) */
override val deposit: PartyAndReference
override val amount: Amount<T>
/** There must be a MoveCommand signed by this key to claim the amount */
override val owner: PublicKey
override val notary: Party
}
// Just for grouping
interface Commands : CommandData {
interface Move : Commands
/**
* Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction
* has a unique ID even when there are no inputs.
*/
interface Issue : Commands { val nonce: Long }
/**
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way.
*/
interface Exit<T> : Commands { val amount: Amount<T> }
}
/** This is the function EVERYONE runs */
override fun verify(tx: TransactionForVerification) {
// Each group is a set of input/output states with distinct issuance definitions. These assets are not fungible
// and must be kept separated for bookkeeping purposes.
val groups = tx.groupStates() { it: FungibleAsset.State<T> -> it.issuanceDef }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val deposit = key.deposit
val token = key.token
val issuer = deposit.party
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
}
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, token, issuer)
} else {
val inputAmount = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for this group")
val outputAmount = outputs.sumFungibleOrZero(token)
// If we want to remove assets from the ledger, that must be signed for by the issuer.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
val exitCommand = tx.commands.select<Commands.Exit<T>>(party = issuer).singleOrNull()
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, token)
requireThat {
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommands<Commands.Move>(inputs, tx)
}
}
}
private fun verifyIssueCommand(inputs: List<State<T>>,
outputs: List<State<T>>,
tx: TransactionForVerification,
issueCommand: AuthenticatedObject<Commands.Issue>,
token: T,
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.
//
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
// sum to more than the inputs. An issuance of zero size is not allowed.
//
// Note that this means literally anyone with access to the network can issue asset claims of arbitrary
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
// external mechanism (such as locally defined rules on which parties are trustworthy).
// The grouping ensures that all outputs have the same deposit reference and token.
val inputAmount = inputs.sumFungibleOrZero(token)
val outputAmount = outputs.sumFungible<T>()
val assetCommands = tx.commands.select<FungibleAsset.Commands>()
requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
"output deposits are owned by a command signer" by (issuer in issueCommand.signingParties)
"output values sum to more than the inputs" by (outputAmount > inputAmount)
"there is only a single issue command" by (assetCommands.count() == 1)
}
}
}
// Small DSL extensions.
/**
* Sums the asset states in the list belonging to a single owner, throwing an exception
* if there are none, or if any of the asset states cannot be added together (i.e. are
* different tokens).
*/
fun <T> Iterable<ContractState>.sumFungibleBy(owner: PublicKey) = filterIsInstance<FungibleAsset.State<T>>().filter { it.owner == owner }.map { it.amount }.sumOrThrow()
/**
* Sums the asset states in the list, throwing an exception if there are none, or if any of the asset
* states cannot be added together (i.e. are different tokens).
*/
fun <T> Iterable<ContractState>.sumFungible() = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrThrow()
/** Sums the asset states in the list, returning null if there are none. */
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrNull()
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: T) = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrZero(token)

View File

@ -8,9 +8,9 @@ import java.util.Currency
/** /**
* Common elements of cash contract states. * Common elements of cash contract states.
*/ */
interface CommonCashState<I : CashIssuanceDefinition> : OwnableState { interface FungibleAssetState<T, I : AssetIssuanceDefinition<T>> : OwnableState {
val issuanceDef: I val issuanceDef: I
/** Where the underlying currency backing this ledger entry can be found (propagated) */ /** Where the underlying currency backing this ledger entry can be found (propagated) */
val deposit: PartyAndReference val deposit: PartyAndReference
val amount: Amount<Currency> val amount: Amount<T>
} }

View File

@ -340,18 +340,18 @@ class IRSTests {
fun `expression calculation testing`() { fun `expression calculation testing`() {
val dummyIRS = singleIRS() val dummyIRS = singleIRS()
val stuffToPrint: ArrayList<String> = arrayListOf( val stuffToPrint: ArrayList<String> = arrayListOf(
"fixedLeg.notional.pennies", "fixedLeg.notional.quantity",
"fixedLeg.fixedRate.ratioUnit", "fixedLeg.fixedRate.ratioUnit",
"fixedLeg.fixedRate.ratioUnit.value", "fixedLeg.fixedRate.ratioUnit.value",
"floatingLeg.notional.pennies", "floatingLeg.notional.quantity",
"fixedLeg.fixedRate", "fixedLeg.fixedRate",
"currentBusinessDate", "currentBusinessDate",
"calculation.floatingLegPaymentSchedule.get(currentBusinessDate)", "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)",
"fixedLeg.notional.token.currencyCode", "fixedLeg.notional.token.currencyCode",
"fixedLeg.notional.pennies * 10", "fixedLeg.notional.quantity * 10",
"fixedLeg.notional.pennies * fixedLeg.fixedRate.ratioUnit.value", "fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value",
"(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ", "(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ",
"(fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value))" "(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))"
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate"
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value", // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value",
//"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", //"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
@ -450,7 +450,7 @@ class IRSTests {
val irs = singleIRS() val irs = singleIRS()
transaction { transaction {
output() { output() {
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(pennies = 0))) irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0)))
} }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
@ -459,7 +459,7 @@ class IRSTests {
transaction { transaction {
output() { output() {
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(pennies = 0))) irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0)))
} }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
@ -487,7 +487,7 @@ class IRSTests {
@Test @Test
fun `ensure same currency notionals`() { fun `ensure same currency notionals`() {
val irs = singleIRS() val irs = singleIRS()
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.pennies, Currency.getInstance("JPY")))) val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.quantity, Currency.getInstance("JPY"))))
transaction { transaction {
output() { output() {
modifiedIRS modifiedIRS
@ -501,7 +501,7 @@ class IRSTests {
@Test @Test
fun `ensure notional amounts are equal`() { fun `ensure notional amounts are equal`() {
val irs = singleIRS() val irs = singleIRS()
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.pennies + 1, irs.floatingLeg.notional.token))) val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.quantity + 1, irs.floatingLeg.notional.token)))
transaction { transaction {
output() { output() {
modifiedIRS modifiedIRS
@ -619,7 +619,7 @@ class IRSTests {
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first() val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey] val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.pennies, Currency.getInstance("JPY"))) var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, Currency.getInstance("JPY")))
output() { output() {
newIRS.copy( newIRS.copy(
@ -640,7 +640,7 @@ class IRSTests {
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) } arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key } val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.pennies, Currency.getInstance("JPY"))) var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
output() { output() {
newIRS.copy( newIRS.copy(

View File

@ -41,7 +41,7 @@ class CashTests {
tweak { tweak {
output { outState } output { outState }
// No command arguments // No command arguments
this `fails requirement` "required com.r3corda.contracts.cash.Cash.Commands.Move command" this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command"
} }
tweak { tweak {
output { outState } output { outState }
@ -52,7 +52,7 @@ class CashTests {
output { outState } output { outState }
output { outState `issued by` MINI_CORP } output { outState `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at least one cash input" this `fails requirement` "at least one asset input"
} }
// Simple reallocation works. // Simple reallocation works.
tweak { tweak {
@ -71,7 +71,7 @@ class CashTests {
output { outState } output { outState }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails requirement` "there is at least one cash input" this `fails requirement` "there is at least one asset input"
} }
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
@ -112,7 +112,7 @@ class CashTests {
// Test issuance from the issuance definition // Test issuance from the issuance definition
val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD) val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD)
val templatePtx = TransactionBuilder() val templatePtx = TransactionBuilder()
Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.pennies, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.quantity, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(templatePtx.inputStates().isEmpty()) assertTrue(templatePtx.inputStates().isEmpty())
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0])
@ -297,7 +297,7 @@ class CashTests {
tweak { tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
this `fails requirement` "required com.r3corda.contracts.cash.Cash.Commands.Move command" this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command"
tweak { tweak {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }

View File

@ -33,40 +33,40 @@ import java.util.*
* *
* @param T the type of the token, for example [Currency]. * @param T the type of the token, for example [Currency].
*/ */
data class Amount<T>(val pennies: Long, val token: T) : Comparable<Amount<T>> { data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
init { init {
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain // Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance. // contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
// If you want to express a negative amount, for now, use a long. // If you want to express a negative amount, for now, use a long.
require(pennies >= 0) { "Negative amounts are not allowed: $pennies" } require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
} }
constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency) constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency)
operator fun plus(other: Amount<T>): Amount<T> { operator fun plus(other: Amount<T>): Amount<T> {
checkCurrency(other) checkCurrency(other)
return Amount(Math.addExact(pennies, other.pennies), token) return Amount(Math.addExact(quantity, other.quantity), token)
} }
operator fun minus(other: Amount<T>): Amount<T> { operator fun minus(other: Amount<T>): Amount<T> {
checkCurrency(other) checkCurrency(other)
return Amount(Math.subtractExact(pennies, other.pennies), token) return Amount(Math.subtractExact(quantity, other.quantity), token)
} }
private fun checkCurrency(other: Amount<T>) { private fun checkCurrency(other: Amount<T>) {
require(other.token == token) { "Currency mismatch: ${other.token} vs $token" } require(other.token == token) { "Currency mismatch: ${other.token} vs $token" }
} }
operator fun div(other: Long): Amount<T> = Amount(pennies / other, token) operator fun div(other: Long): Amount<T> = Amount(quantity / other, token)
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(pennies, other), token) operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), token)
operator fun div(other: Int): Amount<T> = Amount(pennies / other, token) operator fun div(other: Int): Amount<T> = Amount(quantity / other, token)
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(pennies, other.toLong()), token) operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), token)
override fun toString(): String = (BigDecimal(pennies).divide(BigDecimal(100))).setScale(2).toPlainString() override fun toString(): String = (BigDecimal(quantity).divide(BigDecimal(100))).setScale(2).toPlainString()
override fun compareTo(other: Amount<T>): Int { override fun compareTo(other: Amount<T>): Int {
checkCurrency(other) checkCurrency(other)
return pennies.compareTo(other.pennies) return quantity.compareTo(other.quantity)
} }
} }

View File

@ -10,6 +10,8 @@ Here are changes in git master that haven't yet made it to a snapshot release:
* The cash contract has moved from com.r3corda.contracts to com.r3corda.contracts.cash. * The cash contract has moved from com.r3corda.contracts to com.r3corda.contracts.cash.
* Amount class is now generic, to support non-currency types (such as assets, or currency with additional information). * Amount class is now generic, to support non-currency types (such as assets, or currency with additional information).
* Refactored the Cash contract to have a new FungibleAsset superclass, to model all countable assets that can be merged
and split (currency, barrels of oil, etc.)
Milestone 0 Milestone 0

View File

@ -130,7 +130,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer
m.register("WalletBalances.${balance.key}Pennies", newMetric) m.register("WalletBalances.${balance.key}Pennies", newMetric)
newMetric newMetric
} }
metric.pennies = balance.value.pennies metric.pennies = balance.value.quantity
} }
} }
@ -172,7 +172,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer
private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray { private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray {
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt() val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
val amounts = LongArray(numStates) val amounts = LongArray(numStates)
val baseSize = howMuch.pennies / numStates val baseSize = howMuch.quantity / numStates
var filledSoFar = 0L var filledSoFar = 0L
for (i in 0..numStates - 1) { for (i in 0..numStates - 1) {
if (i < numStates - 1) { if (i < numStates - 1) {
@ -181,7 +181,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : SingletonSer
filledSoFar += baseSize filledSoFar += baseSize
} else { } else {
// Handle inexact rounding. // Handle inexact rounding.
amounts[i] = howMuch.pennies - filledSoFar amounts[i] = howMuch.quantity - filledSoFar
} }
} }
return amounts return amounts

View File

@ -2,7 +2,7 @@
"fixedLeg": { "fixedLeg": {
"fixedRatePayer": "Bank A", "fixedRatePayer": "Bank A",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"token": "USD" "token": "USD"
}, },
"paymentFrequency": "SemiAnnual", "paymentFrequency": "SemiAnnual",
@ -27,7 +27,7 @@
"floatingLeg": { "floatingLeg": {
"floatingRatePayer": "Bank B", "floatingRatePayer": "Bank B",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"token": "USD" "token": "USD"
}, },
"paymentFrequency": "Quarterly", "paymentFrequency": "Quarterly",
@ -56,7 +56,7 @@
} }
}, },
"calculation": { "calculation": {
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", "expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
"floatingLegPaymentSchedule": { "floatingLegPaymentSchedule": {
}, },
"fixedLegPaymentSchedule": { "fixedLegPaymentSchedule": {
@ -67,19 +67,19 @@
"eligibleCurrency": "EUR", "eligibleCurrency": "EUR",
"eligibleCreditSupport": "Cash in an Eligible Currency", "eligibleCreditSupport": "Cash in an Eligible Currency",
"independentAmounts": { "independentAmounts": {
"pennies": 0, "quantity": 0,
"token": "EUR" "token": "EUR"
}, },
"threshold": { "threshold": {
"pennies": 0, "quantity": 0,
"token": "EUR" "token": "EUR"
}, },
"minimumTransferAmount": { "minimumTransferAmount": {
"pennies": 25000000, "quantity": 25000000,
"token": "EUR" "token": "EUR"
}, },
"rounding": { "rounding": {
"pennies": 1000000, "quantity": 1000000,
"token": "EUR" "token": "EUR"
}, },
"valuationDate": "Every Local Business Day", "valuationDate": "Every Local Business Day",

View File

@ -350,7 +350,7 @@ class TwoPartyTradeProtocolTests {
@Test @Test
fun `dependency with error on buyer side`() { fun `dependency with error on buyer side`() {
transactionGroupFor<ContractState> { transactionGroupFor<ContractState> {
runWithError(true, false, "at least one cash input") runWithError(true, false, "at least one asset input")
} }
} }

View File

@ -2,7 +2,7 @@
"fixedLeg": { "fixedLeg": {
"fixedRatePayer": "Bank A", "fixedRatePayer": "Bank A",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"currency": "USD" "currency": "USD"
}, },
"paymentFrequency": "SemiAnnual", "paymentFrequency": "SemiAnnual",
@ -27,7 +27,7 @@
"floatingLeg": { "floatingLeg": {
"floatingRatePayer": "Bank B", "floatingRatePayer": "Bank B",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"currency": "USD" "currency": "USD"
}, },
"paymentFrequency": "Quarterly", "paymentFrequency": "Quarterly",
@ -56,7 +56,7 @@
} }
}, },
"calculation": { "calculation": {
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", "expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
"floatingLegPaymentSchedule": { "floatingLegPaymentSchedule": {
}, },
"fixedLegPaymentSchedule": { "fixedLegPaymentSchedule": {
@ -67,19 +67,19 @@
"eligibleCurrency": "EUR", "eligibleCurrency": "EUR",
"eligibleCreditSupport": "Cash in an Eligible Currency", "eligibleCreditSupport": "Cash in an Eligible Currency",
"independentAmounts": { "independentAmounts": {
"pennies": 0, "quantity": 0,
"currency": "EUR" "currency": "EUR"
}, },
"threshold": { "threshold": {
"pennies": 0, "quantity": 0,
"currency": "EUR" "currency": "EUR"
}, },
"minimumTransferAmount": { "minimumTransferAmount": {
"pennies": 25000000, "quantity": 25000000,
"currency": "EUR" "currency": "EUR"
}, },
"rounding": { "rounding": {
"pennies": 1000000, "quantity": 1000000,
"currency": "EUR" "currency": "EUR"
}, },
"valuationDate": "Every Local Business Day", "valuationDate": "Every Local Business Day",