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..b5683f51aa --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/BelongsToContract.kt @@ -0,0 +1,32 @@ +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 write a warning to the log 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 7ad5129538..862c457192 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionState.kt @@ -4,6 +4,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 @@ -26,7 +27,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, /** @@ -50,5 +58,28 @@ 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 fdb255348b..7492641938 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -9,9 +9,10 @@ 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 +import net.corda.core.utilities.warnOnce /** * A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations: @@ -54,23 +55,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 } @@ -88,10 +75,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.warnOnce(""" + 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 * @@ -102,54 +110,78 @@ 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) } } } - /** - * 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) + 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) } } } } - contractInstances.forEach { contract -> - try { - contract.verify(this) - } catch (e: Throwable) { - throw TransactionVerificationException.ContractRejection(id, contract, e) - } + + 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() = 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 9b54509064..bcfd8f3bca 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -17,6 +17,7 @@ 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.loggerFor import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -47,6 +48,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>() @@ -71,7 +77,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) @@ -87,7 +93,6 @@ open class TransactionBuilder @JvmOverloads constructor( else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") } } - return this } // DOCEND 1 @@ -147,10 +152,10 @@ open class TransactionBuilder @JvmOverloads constructor( private fun makeAttachmentConstraint(services: ServicesForResolution, state: TransactionState): AttachmentConstraint { val attachmentId = services.cordappProvider.getContractAttachmentID(state.contract) - ?: throw MissingContractAttachments(listOf(state)) + ?: throw MissingContractAttachments(listOf(state)) val attachmentSigners = services.attachments.openAttachment(attachmentId)?.signers - ?: throw MissingContractAttachments(listOf(state)) + ?: throw MissingContractAttachments(listOf(state)) return when { attachmentSigners.isEmpty() -> HashAttachmentConstraint(attachmentId) @@ -212,7 +217,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) @@ -235,34 +240,37 @@ 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 { @@ -272,20 +280,26 @@ open class TransactionBuilder @JvmOverloads constructor( /** 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 } /** @@ -301,10 +315,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 } /** @@ -316,9 +329,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/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 240edbfd1e..849c937b8e 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -135,3 +135,15 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { throw e.cause!! } +private val warnings = mutableSetOf() + +/** + * Utility to help log a warning message only once. + * It's not thread safe, as in the worst case the warning will be logged twice. + */ +fun Logger.warnOnce(warning: String) { + if (warning !in warnings) { + warnings.add(warning) + this.warn(warning) + } +} \ No newline at end of file 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 ead8da3ae4..62dab98d1e 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -146,6 +146,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 f91eb9c344..67a04ac5f3 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/LedgerTransactionQueryTests.kt @@ -46,11 +46,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 07ef61b49e..d84166613a 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 @@ -250,7 +250,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); @@ -296,7 +296,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); @@ -309,7 +309,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 fc04b31387..26f1f6885b 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 @@ -295,7 +295,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 73681e44b7..e95609abb2 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 @@ -50,6 +50,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 7c5edaeea1..6d31af0ea5 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 @@ -96,11 +96,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) } } @@ -166,11 +166,11 @@ class ObligationTests { transaction { attachments(Obligation.PROGRAM_ID) output(Obligation.PROGRAM_ID, - Obligation.State( - obligor = MINI_CORP, - quantity = 1000.DOLLARS.quantity, - beneficiary = CHARLIE, - template = megaCorpDollarSettlement)) + Obligation.State( + obligor = MINI_CORP, + quantity = 1000.DOLLARS.quantity, + beneficiary = CHARLIE, + template = megaCorpDollarSettlement)) command(MINI_CORP_PUBKEY, Obligation.Commands.Issue()) this.verifies() } @@ -506,10 +506,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())) @@ -525,7 +525,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())) @@ -540,7 +540,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" @@ -554,7 +554,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())) @@ -701,10 +701,10 @@ class ObligationTests { attachments(Obligation.PROGRAM_ID) input(Obligation.PROGRAM_ID, inState) input(Obligation.PROGRAM_ID, - inState.copy( - quantity = 15000, - template = megaCorpPoundSettlement, - beneficiary = AnonymousParty(BOB_PUBKEY))) + inState.copy( + quantity = 15000, + template = megaCorpPoundSettlement, + beneficiary = AnonymousParty(BOB_PUBKEY))) output(Obligation.PROGRAM_ID, outState.copy(quantity = 115000)) command(MINI_CORP_PUBKEY, Obligation.Commands.Move()) this `fails with` "the amounts balance" @@ -962,4 +962,4 @@ class ObligationTests { get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME) private val Amount>.OBLIGATION: Obligation.State get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) -} +} \ No newline at end of file