mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Add commodity contract and test obligations can be settled for it
This commit is contained in:
parent
f72e223f3b
commit
e1d1aed541
@ -0,0 +1,283 @@
|
||||
package com.r3corda.contracts.asset
|
||||
|
||||
import com.r3corda.contracts.clause.AbstractConserveAmount
|
||||
import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.newSecureRandom
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Commodity
|
||||
//
|
||||
|
||||
// Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode.
|
||||
val COMMODITY_PROGRAM_ID = CommodityContract()
|
||||
//SecureHash.sha256("commodity")
|
||||
|
||||
/**
|
||||
* A commodity contract represents an amount of some commodity, tracked on a distributed ledger. The design of this
|
||||
* contract is intentionally similar to the [Cash] contract, and the same commands (issue, move, exit) apply, the
|
||||
* differences are in representation of the underlying commodity. Issuer in this context means the party who has the
|
||||
* commodity, or is otherwise responsible for delivering the commodity on demand, and the deposit reference is use for
|
||||
* internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse).
|
||||
*/
|
||||
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
|
||||
class CommodityContract : ClauseVerifier() {
|
||||
/**
|
||||
* TODO:
|
||||
* 1) hash should be of the contents, not the URI
|
||||
* 2) allow the content to be specified at time of instance creation?
|
||||
*
|
||||
* Motivation: it's the difference between a state object referencing a programRef, which references a
|
||||
* legalContractReference and a state object which directly references both. The latter allows the legal wording
|
||||
* to evolve without requiring code changes. But creates a risk that users create objects governed by a program
|
||||
* that is inconsistent with the legal contract
|
||||
*/
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
||||
|
||||
/**
|
||||
* The clauses for this contract are essentially:
|
||||
*
|
||||
* 1. Group all commodity input and output states in a transaction by issued commodity, and then for each group:
|
||||
* a. Check there are no zero sized output states in the group, and throw an error if so.
|
||||
* b. Check for an issuance command, and do standard issuance checks if so, THEN STOP. Otherwise:
|
||||
* c. Check for a move command (required) and an optional exit command, and that input and output totals are correctly
|
||||
* conserved (output = input - exit)
|
||||
*/
|
||||
interface Clauses {
|
||||
/**
|
||||
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over
|
||||
* each group.
|
||||
*/
|
||||
class Group : GroupClauseVerifier<State, Issued<Commodity>>() {
|
||||
/**
|
||||
* The group clause does not depend on any commands being present, so something has gone terribly wrong if
|
||||
* it doesn't match.
|
||||
*/
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
/**
|
||||
* The group clause is the only top level clause, so end after processing it. If there are any commands left
|
||||
* after this clause has run, the clause verifier will trigger an error.
|
||||
*/
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
// Subclauses to run on each group
|
||||
override val clauses: List<GroupClause<State, Issued<Commodity>>>
|
||||
get() = listOf(
|
||||
NoZeroSizedOutputs<State, Commodity>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
|
||||
/**
|
||||
* Group commodity states by issuance definition (issuer and underlying commodity).
|
||||
*/
|
||||
override fun extractGroups(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Commodity>>>
|
||||
= tx.groupStates<State, Issued<Commodity>> { it.issuanceDef }
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard issue clause, specialised to match the commodity issue command.
|
||||
*/
|
||||
class Issue : AbstractIssue<State, Commodity>({ -> sumCommodities() }, { token: Issued<Commodity> -> sumCommoditiesOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard clause for conserving the amount from input to output.
|
||||
*/
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commodity>()
|
||||
}
|
||||
|
||||
/** A state representing a commodity claim against some party */
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Commodity>>,
|
||||
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
) : FungibleAsset<Commodity> {
|
||||
constructor(deposit: PartyAndReference, amount: Amount<Commodity>, owner: PublicKey)
|
||||
: this(Amount(amount.quantity, Issued<Commodity>(deposit, amount.token)), owner)
|
||||
|
||||
override val deposit: PartyAndReference
|
||||
get() = amount.token.issuer
|
||||
override val contract = COMMODITY_PROGRAM_ID
|
||||
override val exitKeys: Collection<PublicKey>
|
||||
get() = Collections.singleton(owner)
|
||||
override val issuanceDef: Issued<Commodity>
|
||||
get() = amount.token
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Commodity>>, newOwner: PublicKey): FungibleAsset<Commodity>
|
||||
= copy(amount = amount.copy(newAmount.quantity, amount.token), owner = newOwner)
|
||||
|
||||
override fun toString() = "Commodity($amount at $deposit owned by ${owner.toStringShort()})"
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : FungibleAsset.Commands {
|
||||
/**
|
||||
* A command stating that money has been moved, optionally to fulfil another contract.
|
||||
*
|
||||
* @param contractHash the contract this move is for the attention of. Only that contract's verify function
|
||||
* should take the moved states into account when considering whether it is valid. Typically this will be
|
||||
* null.
|
||||
*/
|
||||
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
||||
|
||||
/**
|
||||
* Allows new commodity states to be issued into existence: the nonce ("number used once") ensures the transaction
|
||||
* has a unique ID even when there are no inputs.
|
||||
*/
|
||||
data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands
|
||||
|
||||
/**
|
||||
* 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<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
|
||||
}
|
||||
override val clauses: List<SingleClause>
|
||||
get() = listOf(Clauses.Group())
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<CommodityContract.Commands>()
|
||||
|
||||
/**
|
||||
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
|
||||
*/
|
||||
fun generateIssue(tx: TransactionBuilder, tokenDef: Issued<Commodity>, 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<Issued<Commodity>>, owner: PublicKey, notary: Party) {
|
||||
check(tx.inputStates().isEmpty())
|
||||
check(tx.outputStates().map { it.data }.sumFungibleOrNull<Commodity>() == null)
|
||||
val at = amount.token.issuer
|
||||
tx.addOutputState(TransactionState(CommodityContract.State(amount, owner), notary))
|
||||
tx.addCommand(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<Issued<Commodity>>, to: PublicKey,
|
||||
commodityStates: List<StateAndRef<State>>): List<PublicKey> =
|
||||
generateSpend(tx, Amount(amount.quantity, amount.token.product), to, commodityStates,
|
||||
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.
|
||||
*
|
||||
* @param onlyFromParties if non-null, the wallet will be filtered to only include commodity states issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of commodity claims they are willing to accept.
|
||||
*/
|
||||
// TODO: These spend functions should be shared with [Cash], possibly through some common superclass
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateSpend(tx: TransactionBuilder, amount: Amount<Commodity>, to: PublicKey,
|
||||
commodityStates: List<StateAndRef<State>>, onlyFromParties: Set<Party>? = null): List<PublicKey> {
|
||||
// Discussion
|
||||
//
|
||||
// This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline.
|
||||
//
|
||||
// First we must select a set of commodity states (which for convenience we will call 'coins' here, as in bitcoinj).
|
||||
// The input states can be considered our "wallet", and may consist of coins of different currencies, and from
|
||||
// different institutions and deposits.
|
||||
//
|
||||
// Coin selection is a complex problem all by itself and many different approaches can be used. It is easily
|
||||
// possible for different actors to use different algorithms and approaches that, for example, compete on
|
||||
// privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of
|
||||
// obfuscation and so on.
|
||||
//
|
||||
// Having selected coins of the right currency, we must craft output states for the amount we're sending and
|
||||
// the "change", which goes back to us. The change is required to make the amounts balance. We may need more
|
||||
// than one change output in order to avoid merging coins from different deposits. The point of this design
|
||||
// is to ensure that ledger entries are immutable and globally identifiable.
|
||||
//
|
||||
// Finally, we add the states to the provided partial transaction.
|
||||
|
||||
val currency = amount.token
|
||||
val acceptableCoins = run {
|
||||
val ofCurrency = commodityStates.filter { it.state.data.amount.token.product == currency }
|
||||
if (onlyFromParties != null)
|
||||
ofCurrency.filter { it.state.data.deposit.party in onlyFromParties }
|
||||
else
|
||||
ofCurrency
|
||||
}
|
||||
|
||||
val gathered = arrayListOf<StateAndRef<State>>()
|
||||
var gatheredAmount = Amount(0, currency)
|
||||
var takeChangeFrom: StateAndRef<State>? = null
|
||||
for (c in acceptableCoins) {
|
||||
if (gatheredAmount >= amount) break
|
||||
gathered.add(c)
|
||||
gatheredAmount += Amount(c.state.data.amount.quantity, currency)
|
||||
takeChangeFrom = c
|
||||
}
|
||||
|
||||
if (gatheredAmount < amount)
|
||||
throw InsufficientBalanceException(amount - gatheredAmount)
|
||||
|
||||
val change = if (takeChangeFrom != null && gatheredAmount > amount) {
|
||||
Amount<Issued<Commodity>>(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val keysUsed = gathered.map { it.state.data.owner }.toSet()
|
||||
|
||||
val states = gathered.groupBy { it.state.data.deposit }.map {
|
||||
val coins = it.value
|
||||
val totalAmount = coins.map { it.state.data.amount }.sumOrThrow()
|
||||
TransactionState(State(totalAmount, to), coins.first().state.notary)
|
||||
}
|
||||
|
||||
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.
|
||||
val changeKey = gathered.first().state.data.owner
|
||||
// Add a change output and adjust the last output downwards.
|
||||
states.subList(0, states.lastIndex) +
|
||||
states.last().let { TransactionState(it.data.copy(amount = it.data.amount - change), it.notary) } +
|
||||
TransactionState(State(change, changeKey), gathered.last().state.notary)
|
||||
} else states
|
||||
|
||||
for (state in gathered) tx.addInputState(state)
|
||||
for (state in outputs) tx.addOutputState(state)
|
||||
// What if we already have a move command with the right keys? Filter it out here or in platform code?
|
||||
val keysList = keysUsed.toList()
|
||||
tx.addCommand(Commands.Move(), keysList)
|
||||
return keysList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums the cash states in the list, throwing an exception if there are none, or if any of the cash
|
||||
* states cannot be added together (i.e. are different currencies).
|
||||
*/
|
||||
fun Iterable<ContractState>.sumCommodities() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrThrow()
|
||||
|
||||
/** Sums the cash states in the list, returning null if there are none. */
|
||||
fun Iterable<ContractState>.sumCommoditiesOrNull() = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the cash states in the list, returning zero of the given currency if there are none. */
|
||||
fun Iterable<ContractState>.sumCommoditiesOrZero(currency: Issued<Commodity>) = filterIsInstance<CommodityContract.State>().map { it.amount }.sumOrZero<Issued<Commodity>>(currency)
|
@ -4,7 +4,7 @@ import com.r3corda.core.contracts.*
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class InsufficientBalanceException(val amountMissing: Amount<Currency>) : Exception()
|
||||
class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception()
|
||||
|
||||
/**
|
||||
* Interface for contract states representing assets which are fungible, countable and issued by a
|
||||
|
@ -2,6 +2,7 @@ package com.r3corda.contracts.asset
|
||||
|
||||
import com.r3corda.contracts.asset.Obligation.Lifecycle
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.NullPublicKey
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.testing.*
|
||||
@ -36,7 +37,7 @@ class ObligationTests {
|
||||
)
|
||||
val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2)
|
||||
|
||||
private fun obligationTestRoots(
|
||||
private fun cashObligationTestRoots(
|
||||
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||
) = group.apply {
|
||||
unverifiedTransaction {
|
||||
@ -331,7 +332,7 @@ class ObligationTests {
|
||||
fun `close-out netting`() {
|
||||
// Try netting out two obligations
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
@ -346,7 +347,7 @@ class ObligationTests {
|
||||
// Try netting out two obligations, with the third uninvolved obligation left
|
||||
// as-is
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
@ -361,7 +362,7 @@ class ObligationTests {
|
||||
|
||||
// Try having outputs mis-match the inputs
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
@ -374,7 +375,7 @@ class ObligationTests {
|
||||
|
||||
// Have the wrong signature on the transaction
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
@ -389,7 +390,7 @@ class ObligationTests {
|
||||
fun `payment netting`() {
|
||||
// Try netting out two obligations
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
@ -403,7 +404,7 @@ class ObligationTests {
|
||||
// Try netting out two obligations, but only provide one signature. Unlike close-out netting, we need both
|
||||
// signatures for payment netting
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
@ -415,7 +416,7 @@ class ObligationTests {
|
||||
|
||||
// Multilateral netting, A -> B -> C which can net down to A -> C
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||
@ -429,7 +430,7 @@ class ObligationTests {
|
||||
|
||||
// Multilateral netting without the key of the receiving party
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Issuance") {
|
||||
input("Bob's $1,000,000 obligation to Alice")
|
||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||
@ -442,10 +443,10 @@ class ObligationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `settlement`() {
|
||||
fun `cash settlement`() {
|
||||
// Try settling an obligation
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Settlement") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
input("Alice's $1,000,000")
|
||||
@ -484,11 +485,35 @@ class ObligationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `commodity settlement`() {
|
||||
val defaultFcoj = FCOJ `issued by` defaultIssuer
|
||||
val oneUnitFcoj = Amount(1, defaultFcoj)
|
||||
val obligationDef = Obligation.Terms<Commodity>(nonEmptySetOf(CommodityContract().legalContractReference), nonEmptySetOf(defaultFcoj), TEST_TX_TIME)
|
||||
val oneUnitFcojObligation = Obligation.State(Obligation.Lifecycle.NORMAL, ALICE,
|
||||
obligationDef, oneUnitFcoj.quantity, NullPublicKey)
|
||||
// Try settling a simple commodity obligation
|
||||
ledger {
|
||||
unverifiedTransaction {
|
||||
output("Alice's 1 FCOJ obligation to Bob", oneUnitFcojObligation between Pair(ALICE, BOB_PUBKEY))
|
||||
output("Alice's 1 FCOJ", CommodityContract.State(oneUnitFcoj, ALICE_PUBKEY))
|
||||
}
|
||||
transaction("Settlement") {
|
||||
input("Alice's 1 FCOJ obligation to Bob")
|
||||
input("Alice's 1 FCOJ")
|
||||
output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB_PUBKEY) }
|
||||
command(ALICE_PUBKEY) { Obligation.Commands.Settle<Commodity>(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.issuanceDef)) }
|
||||
command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation<Commodity>().legalContractReference) }
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `payment default`() {
|
||||
// Try defaulting an obligation without a timestamp
|
||||
ledger {
|
||||
obligationTestRoots(this)
|
||||
cashObligationTestRoots(this)
|
||||
transaction("Settlement") {
|
||||
input("Alice's $1,000,000 obligation to Bob")
|
||||
output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) }
|
||||
|
@ -19,27 +19,32 @@ import java.util.*
|
||||
//// Currencies ///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
fun currency(code: String) = Currency.getInstance(code)!!
|
||||
fun commodity(code: String) = Commodity.getInstance(code)!!
|
||||
|
||||
@JvmField val USD = currency("USD")
|
||||
@JvmField val GBP = currency("GBP")
|
||||
@JvmField val CHF = currency("CHF")
|
||||
@JvmField val FCOJ = commodity("FCOJ")
|
||||
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = Amount((amount * 100).toLong(), USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, CHF)
|
||||
fun FCOJ(amount: Int): Amount<Commodity> = Amount(amount.toLong() * 100, FCOJ)
|
||||
|
||||
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
|
||||
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
|
||||
val Int.FCOJ: Amount<Commodity> get() = FCOJ(this)
|
||||
|
||||
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Commodity.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued<Currency>(deposit, this)
|
||||
infix fun Commodity.issuedBy(deposit: PartyAndReference) = Issued<Commodity>(deposit, this)
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, token.issuedBy(deposit))
|
||||
|
||||
|
||||
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class Requirements {
|
||||
|
Loading…
Reference in New Issue
Block a user