From 6cb4310f92d51ae41f8e5415b546a5214cb16621 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Tue, 28 Aug 2018 11:38:33 +0100 Subject: [PATCH] ENT-2320 Introduce annotation to link state classes and contract classes (#1336) * Introduce and check @BelongsToContract annotation * Fix broken ObligationTests * TransactionContractConflictException inherits from TransactionVerificationException * Really fix broken ObligationTests * Convert fun to val * Update kdoc on BelongsToContract, simplify contract checking * Warn in TransactionBuilder if ContractState has no owning contract * Fix failing tests * Unseal TransactionVerificationException * Make contract parameter of TransactionState optional * Replace exception with a warning for now * Update api-current to permit @BelongsToContract annotation to be added * cosmetic tweaks * Throw IllegalArgumentException, not NPE * Throw IllegalArgumentException, not NPE --- .ci/api-current.txt | 3 + .../corda/core/contracts/BelongsToContract.kt | 36 +++++ .../corda/core/contracts/TransactionState.kt | 36 ++++- .../core/transactions/LedgerTransaction.kt | 126 +++++++++++------- .../core/transactions/TransactionBuilder.kt | 57 ++++---- .../core/flows/ContractUpgradeFlowTest.kt | 1 + .../LedgerTransactionQueryTests.kt | 3 +- .../tutorial/testdsl/CommercialPaperTest.java | 6 +- .../docs/tutorial/testdsl/TutorialTestDSL.kt | 2 +- .../net/corda/finance/contracts/asset/Cash.kt | 1 + .../contracts/asset/ObligationTests.kt | 14 +- .../TestCheckScheduleManagementFlow.kt | 1 - 12 files changed, 200 insertions(+), 86 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt diff --git a/.ci/api-current.txt b/.ci/api-current.txt index b0e292868f..e30e90cdcf 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -453,6 +453,9 @@ public final class net.corda.core.contracts.AutomaticHashConstraint extends java public boolean isSatisfiedBy(net.corda.core.contracts.Attachment) public static final net.corda.core.contracts.AutomaticHashConstraint INSTANCE ## +public @interface net.corda.core.contracts.BelongsToContract + public abstract Class value() +## @CordaSerializable public final class net.corda.core.contracts.Command extends java.lang.Object public (T, java.security.PublicKey) diff --git a/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt b/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt new file mode 100644 index 0000000000..d483f8b36b --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt @@ -0,0 +1,36 @@ +package net.corda.core.contracts + +import kotlin.reflect.KClass + +/** + * This annotation is required by any [ContractState] which needs to ensure that it is only ever processed as part of a + * [TransactionState] referencing the specified [Contract]. It may be omitted in the case that the [ContractState] class + * is defined as an inner class of its owning [Contract] class, in which case the "X belongs to Y" relationship is taken + * to be implicitly declared. + * + * During verification of transactions, prior to their being written into the ledger, all input and output states are + * checked to ensure that their [ContractState]s match with their [Contract]s as specified either by this annotation, or + * by their inner/outer class relationship. + * + * The transaction will fail with a [TransactionContractConflictException] if any mismatch is detected. + * + * @param value The class of the [Contract] to which states of the annotated [ContractState] belong. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class BelongsToContract(val value: KClass) + +/** + * Obtain the typename of the required [ContractClass] associated with the target [ContractState], using the + * [BelongsToContract] annotation by default, but falling through to checking the state's enclosing class if there is + * one and it inherits from [Contract]. + */ +val ContractState.requiredContractClassName: String? get() { + val annotation = javaClass.getAnnotation(BelongsToContract::class.java) + if (annotation != null) { + return annotation.value.java.typeName + } + + val enclosingClass = javaClass.enclosingClass ?: return null + return if (Contract::class.java.isAssignableFrom(enclosingClass)) enclosingClass.typeName else null +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt index 760e2f2ee9..a909f21b84 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt @@ -14,6 +14,7 @@ package net.corda.core.contracts import net.corda.core.KeepForDJVM import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.loggerFor // DOCSTART 1 typealias ContractClassName = String @@ -36,7 +37,14 @@ data class TransactionState @JvmOverloads constructor( * sent across, and run, from the network from within a sandbox environment. */ // TODO: Implement the contract sandbox loading of the contract attachments - val contract: ContractClassName, + val contract: ContractClassName = requireNotNull(data.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. + """ + Unable to infer Contract class name because state class ${data::class.java.name} is not annotated with + @BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${data::class.java.name} + with @BelongsToContract, or supply an explicit contract parameter to TransactionState(). + """.trimIndent().replace('\n', ' ') + }, /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ val notary: Party, /** @@ -60,5 +68,29 @@ data class TransactionState @JvmOverloads constructor( /** * A validator for the contract attachments on the transaction. */ - val constraint: AttachmentConstraint = AutomaticHashConstraint) + val constraint: AttachmentConstraint = AutomaticHashConstraint) { + + private companion object { + val logger = loggerFor>() + } + + init { + when { + data.requiredContractClassName == null -> logger.warn( + """ + State class ${data::class.java.name} is not annotated with @BelongsToContract, + and does not have an enclosing class which implements Contract. Annotate ${data::class.java.simpleName} + with @BelongsToContract(${contract.split("\\.\\$").last()}.class) to remove this warning. + """.trimIndent().replace('\n', ' ') + ) + data.requiredContractClassName != contract -> logger.warn( + """ + State class ${data::class.java.name} belongs to contract ${data.requiredContractClassName}, + but is bundled with contract $contract in TransactionState. Annotate ${data::class.java.simpleName} + with @BelongsToContract(${contract.split("\\.\\$").last()}.class) to remove this warning. + """.trimIndent().replace('\n', ' ') + ) + } + } +} // DOCEND 1 diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 01516cc9aa..7459ade177 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -19,7 +19,7 @@ import net.corda.core.internal.castIfPossible import net.corda.core.internal.uncheckedCast import net.corda.core.node.NetworkParameters import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.Try +import net.corda.core.utilities.loggerFor import java.util.* import java.util.function.Predicate @@ -64,23 +64,9 @@ data class LedgerTransaction @JvmOverloads constructor( } private companion object { - private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try> { - return Try.on { - (classLoader ?: this::class.java.classLoader) - .loadClass(className) - .asSubclass(Contract::class.java) - } - } - - private fun stateToContractClass(state: TransactionState): Try> { - return contractClassFor(state.contract, state.data::class.java.classLoader) - } + val logger = loggerFor() } - // Input reference state contracts are not required for verification. - private val contracts: Map>> = (inputs.map { it.state } + outputs) - .map { it.contract to stateToContractClass(it) }.toMap() - val inputStates: List get() = inputs.map { it.state.data } val referenceStates: List get() = references.map { it.state.data } @@ -98,10 +84,31 @@ data class LedgerTransaction @JvmOverloads constructor( */ @Throws(TransactionVerificationException::class) fun verify() { + validateStatesAgainstContract() verifyConstraints() verifyContracts() } + private fun allStates() = inputs.asSequence().map { it.state } + outputs.asSequence() + + /** + * For all input and output [TransactionState]s, validates that the wrapped [ContractState] matches up with the + * wrapped [Contract], as declared by the [BelongsToContract] annotation on the [ContractState]'s class. + * + * A warning will be written to the log if any mismatch is detected. + */ + private fun validateStatesAgainstContract() = allStates().forEach(::validateStateAgainstContract) + + private fun validateStateAgainstContract(state: TransactionState) { + state.data.requiredContractClassName?.let { requiredContractClassName -> + if (state.contract != requiredContractClassName) + logger.warn(""" + State of class ${state.data::class.java.typeName} belongs to contract $requiredContractClassName, but + is bundled in TransactionState with ${state.contract}. + """.trimIndent().replace('\n', ' ')) + } + } + /** * Verify that all contract constraints are valid for each state before running any contract code * @@ -112,54 +119,77 @@ data class LedgerTransaction @JvmOverloads constructor( * @throws TransactionVerificationException if the constraints fail to verify */ private fun verifyConstraints() { - val contractAttachments = attachments.filterIsInstance() - (inputs.map { it.state } + outputs).forEach { state -> - val stateAttachments = contractAttachments.filter { state.contract in it.allContracts } - if (stateAttachments.isEmpty()) throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) + val contractAttachmentsByContract = getUniqueContractAttachmentsByContract() - val uniqueAttachmentsForStateContract = stateAttachments.distinctBy { it.id } + for (state in allStates()) { + val contractAttachment = contractAttachmentsByContract[state.contract] ?: + throw TransactionVerificationException.MissingAttachmentRejection(id, state.contract) - // In case multiple attachments have been added for the same contract, fail because this transaction will not be able to be verified - // because it will break the no-overlap rule that we have implemented in our Classloaders - if (uniqueAttachmentsForStateContract.size > 1) { - throw TransactionVerificationException.ConflictingAttachmentsRejection(id, state.contract) - } + val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, + networkParameters?.whitelistedContractImplementations) - val contractAttachment = uniqueAttachmentsForStateContract.first() - val constraintAttachment = AttachmentWithContext(contractAttachment, state.contract, networkParameters?.whitelistedContractImplementations) if (!state.constraint.isSatisfiedBy(constraintAttachment)) { throw TransactionVerificationException.ContractConstraintRejection(id, state.contract) } } } + private fun getUniqueContractAttachmentsByContract(): Map { + val result = mutableMapOf() + + for (attachment in attachments) { + if (attachment !is ContractAttachment) continue + + for (contract in attachment.allContracts) { + result.compute(contract) { _, previousAttachment -> + when { + previousAttachment == null -> attachment + attachment.id == previousAttachment.id -> previousAttachment + // In case multiple attachments have been added for the same contract, fail because this + // transaction will not be able to be verified because it will break the no-overlap rule + // that we have implemented in our Classloaders + else -> throw TransactionVerificationException.ConflictingAttachmentsRejection(id, contract) + } + } + } + } + + return result + } + /** * Check the transaction is contract-valid by running the verify() for each input and output state contract. * If any contract fails to verify, the whole transaction is considered to be invalid. */ - private fun verifyContracts() { - val contractInstances = ArrayList(contracts.size) - for ((key, result) in contracts) { - when (result) { - is Try.Failure -> throw TransactionVerificationException.ContractCreationError(id, key, result.exception) - is Try.Success -> { - try { - contractInstances.add(result.value.newInstance()) - } catch (e: Throwable) { - throw TransactionVerificationException.ContractCreationError(id, result.value.name, e) - } - } - } - } - contractInstances.forEach { contract -> - try { - contract.verify(this) - } catch (e: Throwable) { - throw TransactionVerificationException.ContractRejection(id, contract, e) - } + private fun verifyContracts() = allStates().forEach { ts -> + val contractClass = getContractClass(ts) + val contract = createContractInstance(contractClass) + + try { + contract.verify(this) + } catch (e: Exception) { + throw TransactionVerificationException.ContractRejection(id, contract, e) } } + // Obtain the contract class from the class name, wrapping any exception as a [ContractCreationError] + private fun getContractClass(ts: TransactionState): Class = + try { + (ts.data::class.java.classLoader ?: this::class.java.classLoader) + .loadClass(ts.contract) + .asSubclass(Contract::class.java) + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(id, ts.contract, e) + } + + // Obtain an instance of the contract class, wrapping any exception as a [ContractCreationError] + private fun createContractInstance(contractClass: Class): Contract = + try { + contractClass.newInstance() + } catch (e: Exception) { + throw TransactionVerificationException.ContractCreationError(id, contractClass.name, e) + } + /** * Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there * are any inputs or reference inputs, all outputs must have the same notary. diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 523ec4c721..5c3a85b637 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -29,6 +29,8 @@ import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.loggerFor import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -59,6 +61,11 @@ open class TransactionBuilder @JvmOverloads constructor( protected var privacySalt: PrivacySalt = PrivacySalt(), protected val references: MutableList = arrayListOf() ) { + + private companion object { + val logger = loggerFor() + } + private val inputsWithTransactionState = arrayListOf>() private val referencesWithTransactionState = arrayListOf>() @@ -83,7 +90,7 @@ open class TransactionBuilder @JvmOverloads constructor( // DOCSTART 1 /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ - fun withItems(vararg items: Any): TransactionBuilder { + fun withItems(vararg items: Any) = apply { for (t in items) { when (t) { is StateAndRef<*> -> addInputState(t) @@ -99,7 +106,6 @@ open class TransactionBuilder @JvmOverloads constructor( else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } } - return this } // DOCEND 1 @@ -205,7 +211,7 @@ open class TransactionBuilder @JvmOverloads constructor( * Note: Reference states are only supported on Corda networks running a minimum platform version of 4. * [toWireTransaction] will throw an [IllegalStateException] if called in such an environment. */ - open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder { + open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>) = apply { val stateAndRef = referencedStateAndRef.stateAndRef referencesWithTransactionState.add(stateAndRef.state) @@ -228,57 +234,64 @@ open class TransactionBuilder @JvmOverloads constructor( checkNotary(stateAndRef) references.add(stateAndRef.ref) checkForInputsAndReferencesOverlap() - return this } /** Adds an input [StateRef] to the transaction. */ - open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder { + open fun addInputState(stateAndRef: StateAndRef<*>) = apply { checkNotary(stateAndRef) inputs.add(stateAndRef.ref) inputsWithTransactionState.add(stateAndRef.state) - return this } /** Adds an attachment with the specified hash to the TransactionBuilder. */ - fun addAttachment(attachmentId: SecureHash): TransactionBuilder { + fun addAttachment(attachmentId: SecureHash) = apply { attachments.add(attachmentId) - return this } /** Adds an output state to the transaction. */ - fun addOutputState(state: TransactionState<*>): TransactionBuilder { + fun addOutputState(state: TransactionState<*>) = apply { outputs.add(state) - return this } /** Adds an output state, with associated contract code (and constraints), and notary, to the transaction. */ @JvmOverloads fun addOutputState( state: ContractState, - contract: ContractClassName, + contract: ContractClassName = requireNotNull(state.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. +""" +Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with +@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} +with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). +""".trimIndent().replace('\n', ' ') + }, notary: Party, encumbrance: Int? = null, constraint: AttachmentConstraint = AutomaticHashConstraint - ): TransactionBuilder { - return addOutputState(TransactionState(state, contract, notary, encumbrance, constraint)) - } + ) = addOutputState(TransactionState(state, contract, notary, encumbrance, constraint)) /** A default notary must be specified during builder construction to use this method */ @JvmOverloads fun addOutputState( - state: ContractState, contract: ContractClassName, + state: ContractState, + contract: ContractClassName = requireNotNull(state.requiredContractClassName) { + //TODO: add link to docsite page, when there is one. +""" +Unable to infer Contract class name because state class ${state::class.java.name} is not annotated with +@BelongsToContract, and does not have an enclosing class which implements Contract. Either annotate ${state::class.java.name} +with @BelongsToContract, or supply an explicit contract parameter to addOutputState(). +""".trimIndent().replace('\n', ' ') + }, constraint: AttachmentConstraint = AutomaticHashConstraint - ): TransactionBuilder { + ) = apply { checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" } addOutputState(state, contract, notary!!, constraint = constraint) - return this } /** Adds a [Command] to the transaction. */ - fun addCommand(arg: Command<*>): TransactionBuilder { + fun addCommand(arg: Command<*>) = apply { commands.add(arg) - return this } /** @@ -293,10 +306,9 @@ open class TransactionBuilder @JvmOverloads constructor( * transaction must then be signed by the notary service within this window of time. In this way, the notary acts as * the Timestamp Authority. */ - fun setTimeWindow(timeWindow: TimeWindow): TransactionBuilder { + fun setTimeWindow(timeWindow: TimeWindow) = apply { check(notary != null) { "Only notarised transactions can have a time-window" } window = timeWindow - return this } /** @@ -308,9 +320,8 @@ open class TransactionBuilder @JvmOverloads constructor( */ fun setTimeWindow(time: Instant, timeTolerance: Duration) = setTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) - fun setPrivacySalt(privacySalt: PrivacySalt): TransactionBuilder { + fun setPrivacySalt(privacySalt: PrivacySalt) = apply { this.privacySalt = privacySalt - return this } /** Returns an immutable list of input [StateRef]s. */ diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 9a6deff59a..ac0c1c6aa5 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -156,6 +156,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality { class Move : TypeOnlyCommandData() + @BelongsToContract(CashV2::class) data class State(override val amount: Amount>, val owners: List) : FungibleAsset { override val owner: AbstractParty = owners.first() override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet() diff --git a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt index e31dfe063c..2338f302e5 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -56,11 +56,12 @@ class LedgerTransactionQueryTests { data class Cmd3(val id: Int) : CommandData, Commands // Unused command, required for command not-present checks. } - + @BelongsToContract(DummyContract::class) private class StringTypeDummyState(val data: String) : ContractState { override val participants: List = emptyList() } + @BelongsToContract(DummyContract::class) private class IntTypeDummyState(val data: Int) : ContractState { override val participants: List = emptyList() } diff --git a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java index 1e5ba761d8..7d8f31eb2d 100644 --- a/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java +++ b/docs/source/example-code/src/test/java/net/corda/docs/java/tutorial/testdsl/CommercialPaperTest.java @@ -260,7 +260,7 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); @@ -306,7 +306,7 @@ public class CommercialPaperTest { // Some CP is issued onto the ledger by MegaCorp. l.transaction("Issuance", tx -> { - tx.output(Cash.PROGRAM_ID, "paper", getPaper()); + tx.output(JCP_PROGRAM_ID, "paper", getPaper()); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue()); tx.attachments(JCP_PROGRAM_ID); tx.timeWindow(TEST_TX_TIME); @@ -319,7 +319,7 @@ public class CommercialPaperTest { tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty())); JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper"); tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty())); - tx.command(alice.getPublicKey(), new Cash.Commands.Move()); + tx.command(alice.getPublicKey(), new Cash.Commands.Move(JavaCommercialPaper.class)); tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move()); return tx.verifies(); }); diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt index 2986e8ef39..4c2a1e6a24 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/tutorial/testdsl/TutorialTestDSL.kt @@ -305,7 +305,7 @@ class CommercialPaperTest { input("alice's $900") output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party) output(CP_PROGRAM_ID, "alice's paper", "paper".output().withOwner(alice.party)) - command(alice.publicKey, Cash.Commands.Move()) + command(alice.publicKey, Cash.Commands.Move(CommercialPaper::class.java)) command(megaCorp.publicKey, CommercialPaper.Commands.Move()) verifies() } diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 44ec6b8fc7..51afb22031 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -60,6 +60,7 @@ class Cash : OnLedgerAsset() { // DOCSTART 1 /** A state representing a cash claim against some party. */ + @BelongsToContract(Cash::class) data class State( override val amount: Amount>, diff --git a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index 0eb09e4fc3..a4d18b9cd0 100644 --- a/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -106,11 +106,11 @@ class ObligationTests { group: LedgerDSL ) = group.apply { unverifiedTransaction { - attachments(Obligation.PROGRAM_ID) + attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) output(Obligation.PROGRAM_ID, "Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) output(Obligation.PROGRAM_ID, "Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) output(Obligation.PROGRAM_ID, "MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB)) - output(Obligation.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) + output(Cash.PROGRAM_ID, "Alice's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) } } @@ -516,10 +516,10 @@ class ObligationTests { ledgerServices.ledger(DUMMY_NOTARY) { cashObligationTestRoots(this) transaction("Settlement") { - attachments(Obligation.PROGRAM_ID) + attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -535,7 +535,7 @@ class ObligationTests { input(Obligation.PROGRAM_ID, oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) input(Cash.PROGRAM_ID, 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) output(Obligation.PROGRAM_ID, "Alice's $500,000 obligation to Bob", halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB)) - output(Obligation.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $500,000", 500000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) @@ -550,7 +550,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID, Cash.PROGRAM_ID) input(Obligation.PROGRAM_ID, defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob input(Cash.PROGRAM_ID, 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy ALICE) - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) this `fails with` "all inputs are in the normal state" @@ -564,7 +564,7 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output(Obligation.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) + output(Cash.PROGRAM_ID, "Bob's $1,000,000", 1000000.DOLLARS.CASH issuedBy defaultIssuer ownedBy BOB) command(ALICE_PUBKEY, Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token))) command(ALICE_PUBKEY, Cash.Commands.Move(Obligation::class.java)) attachment(attachment(cashContractBytes.inputStream())) diff --git a/tools/notary-healthcheck/cordapp/src/test/kotlin/net/corda/notaryhealthcheck/cordapp/TestCheckScheduleManagementFlow.kt b/tools/notary-healthcheck/cordapp/src/test/kotlin/net/corda/notaryhealthcheck/cordapp/TestCheckScheduleManagementFlow.kt index 6ebfd3a589..50d74a9641 100644 --- a/tools/notary-healthcheck/cordapp/src/test/kotlin/net/corda/notaryhealthcheck/cordapp/TestCheckScheduleManagementFlow.kt +++ b/tools/notary-healthcheck/cordapp/src/test/kotlin/net/corda/notaryhealthcheck/cordapp/TestCheckScheduleManagementFlow.kt @@ -86,7 +86,6 @@ class TestCheckScheduleManagementFlow { assertEquals(1, pendingChecksAfterCleanUp.states.size, "Expected 1 pending check to be removed, got ${pendingChecksAfterCleanUp.states.size}") } - @Test fun testStoppingTwoForOneTarget() { val target = Monitorable(notary1, notary1)