mirror of
https://github.com/corda/corda.git
synced 2025-01-19 11:16:54 +00:00
ENT-2320 Introduce BelongsToContract annotation (#1)
* ENT-2320 Introduce BelongsToContract annotation * Update kdoc * Eliminate duplicate warnings
This commit is contained in:
parent
63ebc394bf
commit
7ee946b98f
@ -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<? extends net.corda.core.contracts.Contract> value()
|
||||
##
|
||||
@CordaSerializable
|
||||
public final class net.corda.core.contracts.Command extends java.lang.Object
|
||||
public <init>(T, java.security.PublicKey)
|
||||
|
@ -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<out Contract>)
|
||||
/**
|
||||
* 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
|
||||
}
|
@ -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<out T : ContractState> @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<out T : ContractState> @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<TransactionState<*>>()
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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,22 +55,8 @@ data class LedgerTransaction @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private fun contractClassFor(className: ContractClassName, classLoader: ClassLoader?): Try<Class<out Contract>> {
|
||||
return Try.on {
|
||||
(classLoader ?: this::class.java.classLoader)
|
||||
.loadClass(className)
|
||||
.asSubclass(Contract::class.java)
|
||||
val logger = loggerFor<LedgerTransaction>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stateToContractClass(state: TransactionState<ContractState>): Try<Class<out Contract>> {
|
||||
return contractClassFor(state.contract, state.data::class.java.classLoader)
|
||||
}
|
||||
}
|
||||
|
||||
// Input reference state contracts are not required for verification.
|
||||
private val contracts: Map<ContractClassName, Try<Class<out Contract>>> = (inputs.map { it.state } + outputs)
|
||||
.map { it.contract to stateToContractClass(it) }.toMap()
|
||||
|
||||
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
|
||||
val referenceStates: List<ContractState> 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<ContractState>) {
|
||||
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<ContractAttachment>()
|
||||
(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<ContractClassName, ContractAttachment> {
|
||||
val result = mutableMapOf<ContractClassName, ContractAttachment>()
|
||||
|
||||
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<Contract>(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 ->
|
||||
private fun verifyContracts() = allStates().forEach { ts ->
|
||||
val contractClass = getContractClass(ts)
|
||||
val contract = createContractInstance(contractClass)
|
||||
|
||||
try {
|
||||
contract.verify(this)
|
||||
} catch (e: Throwable) {
|
||||
} 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<ContractState>): Class<out Contract> =
|
||||
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<out Contract>): 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.
|
||||
|
@ -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<StateRef> = arrayListOf()
|
||||
) {
|
||||
|
||||
private companion object {
|
||||
val logger = loggerFor<TransactionBuilder>()
|
||||
}
|
||||
|
||||
private val inputsWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
private val referencesWithTransactionState = arrayListOf<TransactionState<ContractState>>()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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. */
|
||||
|
@ -135,3 +135,15 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
|
||||
throw e.cause!!
|
||||
}
|
||||
|
||||
private val warnings = mutableSetOf<String>()
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
@ -146,6 +146,7 @@ class ContractUpgradeFlowTest : WithContracts, WithFinality {
|
||||
|
||||
class Move : TypeOnlyCommandData()
|
||||
|
||||
@BelongsToContract(CashV2::class)
|
||||
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> {
|
||||
override val owner: AbstractParty = owners.first()
|
||||
override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet()
|
||||
|
@ -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<AbstractParty> = emptyList()
|
||||
}
|
||||
|
||||
@BelongsToContract(DummyContract::class)
|
||||
private class IntTypeDummyState(val data: Int) : ContractState {
|
||||
override val participants: List<AbstractParty> = emptyList()
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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<ICommercialPaperState>().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()
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
|
||||
// DOCSTART 1
|
||||
/** A state representing a cash claim against some party. */
|
||||
@BelongsToContract(Cash::class)
|
||||
data class State(
|
||||
override val amount: Amount<Issued<Currency>>,
|
||||
|
||||
|
@ -96,11 +96,11 @@ class ObligationTests {
|
||||
group: LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>
|
||||
) = 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()))
|
||||
|
Loading…
Reference in New Issue
Block a user