mirror of
https://github.com/corda/corda.git
synced 2025-01-29 23:54:07 +00:00
Improve the contracts DSL, make the Cash contract extend the DSL in a small way to make working with arrays of cash outputs easier.
This commit is contained in:
parent
f09c624c0f
commit
163175860d
@ -1,3 +1,5 @@
|
|||||||
|
package contracts
|
||||||
|
|
||||||
import core.*
|
import core.*
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
@ -11,7 +13,7 @@ val CASH_PROGRAM_ID = SecureHash.sha256("cash")
|
|||||||
/** A state representing a claim on the cash reserves of some institution */
|
/** A state representing a claim on the cash reserves of some institution */
|
||||||
data class CashState(
|
data class CashState(
|
||||||
/** 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: DepositPointer,
|
val deposit: InstitutionReference,
|
||||||
|
|
||||||
val amount: Amount,
|
val amount: Amount,
|
||||||
|
|
||||||
@ -35,6 +37,9 @@ class ExitCashCommand(val amount: Amount) : Command {
|
|||||||
|
|
||||||
class InsufficientBalanceException(val amountMissing: Amount) : Exception()
|
class InsufficientBalanceException(val amountMissing: Amount) : Exception()
|
||||||
|
|
||||||
|
// Small DSL extension.
|
||||||
|
fun Iterable<ContractState>.sumCashBy(owner: PublicKey) = this.filterIsInstance<CashState>().filter { it.owner == owner }.map { it.amount }.sum()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@ -50,7 +55,7 @@ class InsufficientBalanceException(val amountMissing: Amount) : Exception()
|
|||||||
*/
|
*/
|
||||||
object CashContract : Contract {
|
object CashContract : Contract {
|
||||||
/** This is the function EVERYONE runs */
|
/** This is the function EVERYONE runs */
|
||||||
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSignedCommand>) {
|
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>) {
|
||||||
val cashInputs = inStates.filterIsInstance<CashState>()
|
val cashInputs = inStates.filterIsInstance<CashState>()
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
@ -77,13 +82,8 @@ object CashContract : Contract {
|
|||||||
val inputAmount = inputs.map { it.amount }.sum()
|
val inputAmount = inputs.map { it.amount }.sum()
|
||||||
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
|
val outputAmount = outputs.map { it.amount }.sumOrZero(currency)
|
||||||
|
|
||||||
val issuerCommand = args.
|
val issuerCommand = args.select<ExitCashCommand>(institution = deposit.institution).singleOrNull()
|
||||||
filter { it.signingInstitution == deposit.institution }.
|
val amountExitingLedger = issuerCommand?.value?.amount ?: Amount(0, inputAmount.currency)
|
||||||
// TODO: this map+filterNotNull pattern will become a single function in the next Kotlin beta.
|
|
||||||
map { it.command as? ExitCashCommand }.
|
|
||||||
filterNotNull().
|
|
||||||
singleOrNull()
|
|
||||||
val amountExitingLedger = issuerCommand?.amount ?: Amount(0, inputAmount.currency)
|
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"for deposit ${deposit.reference} at issuer ${deposit.institution.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
|
"for deposit ${deposit.reference} at issuer ${deposit.institution.name} the amounts balance" by (inputAmount == outputAmount + amountExitingLedger)
|
||||||
@ -96,7 +96,7 @@ object CashContract : Contract {
|
|||||||
// see a signature from each of those keys. The actual signatures have been verified against the transaction
|
// see a signature from each of those keys. The actual signatures have been verified against the transaction
|
||||||
// data by the platform before execution.
|
// data by the platform before execution.
|
||||||
val owningPubKeys = cashInputs.map { it.owner }.toSortedSet()
|
val owningPubKeys = cashInputs.map { it.owner }.toSortedSet()
|
||||||
val keysThatSigned = args.filter { it.command is MoveCashCommand }.map { it.signer }.toSortedSet()
|
val keysThatSigned = args.select<MoveCashCommand>().map { it.signer }.toSortedSet()
|
||||||
requireThat {
|
requireThat {
|
||||||
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
|
"the owning keys are the same as the signing keys" by (owningPubKeys == keysThatSigned)
|
||||||
}
|
}
|
||||||
@ -167,7 +167,7 @@ object CashContract : Contract {
|
|||||||
} else states
|
} else states
|
||||||
|
|
||||||
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
|
// Finally, generate the commands. Pretend to sign here, real signatures aren't done yet.
|
||||||
val commands = keysUsed.map { VerifiedSignedCommand(it, null, MoveCashCommand()) }
|
val commands = keysUsed.map { VerifiedSigned(it, null, MoveCashCommand()) }
|
||||||
|
|
||||||
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
|
return TransactionForTest(gathered.toArrayList(), outputs.toArrayList(), commands.toArrayList())
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.div
|
import kotlin.math.div
|
||||||
|
|
||||||
@ -14,6 +15,17 @@ import kotlin.math.div
|
|||||||
|
|
||||||
// TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
|
// TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
|
||||||
|
|
||||||
|
// region Misc
|
||||||
|
inline fun <reified T : Command> List<VerifiedSigned<Command>>.select(signer: PublicKey? = null, institution: Institution? = null) =
|
||||||
|
filter { it.value is T }.
|
||||||
|
filter { if (signer == null) true else signer == it.signer }.
|
||||||
|
filter { if (institution == null) true else institution == it.signingInstitution }.
|
||||||
|
map { VerifiedSigned<T>(it.signer, it.signingInstitution, it.value as T) }
|
||||||
|
|
||||||
|
inline fun <reified T : Command> List<VerifiedSigned<Command>>.requireSingleCommand() = select<T>().single()
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
// region Currencies
|
// region Currencies
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
fun currency(code: String) = Currency.getInstance(code)
|
fun currency(code: String) = Currency.getInstance(code)
|
||||||
@ -46,7 +58,6 @@ fun requireThat(body: Requirements.() -> Unit) {
|
|||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// region Amounts
|
// region Amounts
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -36,3 +36,10 @@ sealed class DigitalSignature(bits: ByteArray, val covering: Int) : OpaqueBytes(
|
|||||||
class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
|
class LegallyIdentifiable(val signer: Institution, bits: ByteArray, covering: Int) : WithKey(signer.owningKey, bits, covering)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||||
|
override fun getAlgorithm() = "NULL"
|
||||||
|
override fun getEncoded() = byteArrayOf(0)
|
||||||
|
override fun getFormat() = "NULL"
|
||||||
|
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
|
||||||
|
override fun toString() = "NULL_KEY"
|
||||||
|
}
|
||||||
|
@ -76,11 +76,11 @@ data class SignedCommand(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** Obtained from a [SignedCommand], deserialised and signature checked */
|
/** Obtained from a [SignedCommand], deserialised and signature checked */
|
||||||
data class VerifiedSignedCommand(
|
data class VerifiedSigned<out T : Command>(
|
||||||
val signer: PublicKey,
|
val signer: PublicKey,
|
||||||
/** If the public key was recognised, the looked up institution is available here, otherwise it's null */
|
/** If the public key was recognised, the looked up institution is available here, otherwise it's null */
|
||||||
val signingInstitution: Institution?,
|
val signingInstitution: Institution?,
|
||||||
val command: Command
|
val value: T
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,13 +90,13 @@ data class VerifiedSignedCommand(
|
|||||||
*/
|
*/
|
||||||
interface Contract {
|
interface Contract {
|
||||||
/** Must throw an exception if there's a problem that should prevent state transition. */
|
/** Must throw an exception if there's a problem that should prevent state transition. */
|
||||||
fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSignedCommand>)
|
fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to some money being stored by an institution e.g. in a vault or (more likely) on their normal ledger.
|
* Reference to something being stored or issued by an institution e.g. in a vault or (more likely) on their normal
|
||||||
* The deposit reference is intended to be encrypted so it's meaningless to anyone other than the institution.
|
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the institution.
|
||||||
*/
|
*/
|
||||||
data class DepositPointer(val institution: Institution, val reference: OpaqueBytes) {
|
data class InstitutionReference(val institution: Institution, val reference: OpaqueBytes) {
|
||||||
override fun toString() = "${institution.name}$reference"
|
override fun toString() = "${institution.name}$reference"
|
||||||
}
|
}
|
@ -50,11 +50,11 @@ val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
|
|||||||
data class TransactionForTest(
|
data class TransactionForTest(
|
||||||
private val inStates: MutableList<ContractState> = arrayListOf(),
|
private val inStates: MutableList<ContractState> = arrayListOf(),
|
||||||
private val outStates: MutableList<ContractState> = arrayListOf(),
|
private val outStates: MutableList<ContractState> = arrayListOf(),
|
||||||
private val args: MutableList<VerifiedSignedCommand> = arrayListOf()
|
private val args: MutableList<VerifiedSigned<Command>> = arrayListOf()
|
||||||
) {
|
) {
|
||||||
fun input(s: () -> ContractState) = inStates.add(s())
|
fun input(s: () -> ContractState) = inStates.add(s())
|
||||||
fun output(s: () -> ContractState) = outStates.add(s())
|
fun output(s: () -> ContractState) = outStates.add(s())
|
||||||
fun arg(key: PublicKey, c: () -> Command) = args.add(VerifiedSignedCommand(key, TEST_KEYS_TO_CORP_MAP[key], c()))
|
fun arg(key: PublicKey, c: () -> Command) = args.add(VerifiedSigned(key, TEST_KEYS_TO_CORP_MAP[key], c()))
|
||||||
|
|
||||||
infix fun Contract.`fails requirement`(msg: String) {
|
infix fun Contract.`fails requirement`(msg: String) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import contracts.*
|
||||||
import core.*
|
import core.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
@ -9,7 +10,7 @@ import kotlin.test.assertFailsWith
|
|||||||
|
|
||||||
class CashTests {
|
class CashTests {
|
||||||
val inState = CashState(
|
val inState = CashState(
|
||||||
deposit = DepositPointer(MEGA_CORP, OpaqueBytes.of(1)),
|
deposit = InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)),
|
||||||
amount = 1000.DOLLARS,
|
amount = 1000.DOLLARS,
|
||||||
owner = DUMMY_PUBKEY_1
|
owner = DUMMY_PUBKEY_1
|
||||||
)
|
)
|
||||||
@ -222,10 +223,10 @@ class CashTests {
|
|||||||
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
val OUR_PUBKEY_1 = DUMMY_PUBKEY_1
|
||||||
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2
|
||||||
val WALLET = listOf(
|
val WALLET = listOf(
|
||||||
CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
|
CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 100.DOLLARS, OUR_PUBKEY_1),
|
||||||
CashState(DepositPointer(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
|
CashState(InstitutionReference(MEGA_CORP, OpaqueBytes.of(1)), 400.DOLLARS, OUR_PUBKEY_1),
|
||||||
CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
|
CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 80.DOLLARS, OUR_PUBKEY_1),
|
||||||
CashState(DepositPointer(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
|
CashState(InstitutionReference(MINI_CORP, OpaqueBytes.of(2)), 80.SWISS_FRANCS, OUR_PUBKEY_1)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Loading…
x
Reference in New Issue
Block a user