mirror of
https://github.com/corda/corda.git
synced 2024-12-25 07:31:10 +00:00
Merged in rnicoll-obligation-fungible (pull request #199)
Add commodities to provide worked example of a different thing under Obligation
This commit is contained in:
commit
0ac29bec26
@ -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.security.PublicKey
|
||||||
import java.util.*
|
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
|
* 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.contracts.asset.Obligation.Lifecycle
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.NullPublicKey
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.serialization.OpaqueBytes
|
import com.r3corda.core.serialization.OpaqueBytes
|
||||||
import com.r3corda.core.testing.*
|
import com.r3corda.core.testing.*
|
||||||
@ -36,7 +37,7 @@ class ObligationTests {
|
|||||||
)
|
)
|
||||||
val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2)
|
val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2)
|
||||||
|
|
||||||
private fun obligationTestRoots(
|
private fun cashObligationTestRoots(
|
||||||
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||||
) = group.apply {
|
) = group.apply {
|
||||||
unverifiedTransaction {
|
unverifiedTransaction {
|
||||||
@ -331,7 +332,7 @@ class ObligationTests {
|
|||||||
fun `close-out netting`() {
|
fun `close-out netting`() {
|
||||||
// Try netting out two obligations
|
// Try netting out two obligations
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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
|
// Try netting out two obligations, with the third uninvolved obligation left
|
||||||
// as-is
|
// as-is
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
@ -361,7 +362,7 @@ class ObligationTests {
|
|||||||
|
|
||||||
// Try having outputs mis-match the inputs
|
// Try having outputs mis-match the inputs
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
@ -374,7 +375,7 @@ class ObligationTests {
|
|||||||
|
|
||||||
// Have the wrong signature on the transaction
|
// Have the wrong signature on the transaction
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
@ -389,7 +390,7 @@ class ObligationTests {
|
|||||||
fun `payment netting`() {
|
fun `payment netting`() {
|
||||||
// Try netting out two obligations
|
// Try netting out two obligations
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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
|
// Try netting out two obligations, but only provide one signature. Unlike close-out netting, we need both
|
||||||
// signatures for payment netting
|
// signatures for payment netting
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
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
|
// Multilateral netting, A -> B -> C which can net down to A -> C
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||||
@ -429,7 +430,7 @@ class ObligationTests {
|
|||||||
|
|
||||||
// Multilateral netting without the key of the receiving party
|
// Multilateral netting without the key of the receiving party
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Issuance") {
|
transaction("Issuance") {
|
||||||
input("Bob's $1,000,000 obligation to Alice")
|
input("Bob's $1,000,000 obligation to Alice")
|
||||||
input("MegaCorp's $1,000,000 obligation to Bob")
|
input("MegaCorp's $1,000,000 obligation to Bob")
|
||||||
@ -442,10 +443,10 @@ class ObligationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `settlement`() {
|
fun `cash settlement`() {
|
||||||
// Try settling an obligation
|
// Try settling an obligation
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Settlement") {
|
transaction("Settlement") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
input("Alice's $1,000,000 obligation to Bob")
|
||||||
input("Alice's $1,000,000")
|
input("Alice's $1,000,000")
|
||||||
@ -497,11 +498,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
|
@Test
|
||||||
fun `payment default`() {
|
fun `payment default`() {
|
||||||
// Try defaulting an obligation without a timestamp
|
// Try defaulting an obligation without a timestamp
|
||||||
ledger {
|
ledger {
|
||||||
obligationTestRoots(this)
|
cashObligationTestRoots(this)
|
||||||
transaction("Settlement") {
|
transaction("Settlement") {
|
||||||
input("Alice's $1,000,000 obligation to Bob")
|
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) }
|
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 ///////////////////////////////////////////////////////////////////////////////////////////////////////
|
//// Currencies ///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
fun currency(code: String) = Currency.getInstance(code)!!
|
fun currency(code: String) = Currency.getInstance(code)!!
|
||||||
|
fun commodity(code: String) = Commodity.getInstance(code)!!
|
||||||
|
|
||||||
@JvmField val USD = currency("USD")
|
@JvmField val USD = currency("USD")
|
||||||
@JvmField val GBP = currency("GBP")
|
@JvmField val GBP = currency("GBP")
|
||||||
@JvmField val CHF = currency("CHF")
|
@JvmField val CHF = currency("CHF")
|
||||||
|
@JvmField val FCOJ = commodity("FCOJ")
|
||||||
|
|
||||||
fun DOLLARS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, USD)
|
fun DOLLARS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, USD)
|
||||||
fun DOLLARS(amount: Double): Amount<Currency> = Amount((amount * 100).toLong(), USD)
|
fun DOLLARS(amount: Double): Amount<Currency> = Amount((amount * 100).toLong(), USD)
|
||||||
fun POUNDS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, GBP)
|
fun POUNDS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, GBP)
|
||||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = Amount(amount.toLong() * 100, CHF)
|
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 Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||||
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||||
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
|
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
|
||||||
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(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 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 Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||||
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued<Currency>(deposit, this)
|
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))
|
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, token.issuedBy(deposit))
|
||||||
|
|
||||||
|
|
||||||
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
class Requirements {
|
class Requirements {
|
||||||
|
@ -420,3 +420,16 @@ enum class NetType {
|
|||||||
*/
|
*/
|
||||||
PAYMENT
|
PAYMENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Commodity(val symbol: String,
|
||||||
|
val displayName: String,
|
||||||
|
val commodityCode: String = symbol,
|
||||||
|
val defaultFractionDigits: Int = 0) {
|
||||||
|
companion object {
|
||||||
|
private val registry = mapOf<String, Commodity>(
|
||||||
|
Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice"))
|
||||||
|
)
|
||||||
|
fun getInstance(symbol: String): Commodity?
|
||||||
|
= registry[symbol]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user