diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt index 0d80a1cc92..9860144d3c 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/TransactionBuilder.kt @@ -20,16 +20,31 @@ import java.util.* * an output state can be added by just passing in a [ContractState] – a [TransactionState] with the * default notary will be generated automatically. */ -abstract class TransactionBuilder(protected val type: TransactionType = TransactionType.General(), - protected val notary: Party? = null) { - protected val inputs: MutableList = arrayListOf() - protected val attachments: MutableList = arrayListOf() - protected val outputs: MutableList> = arrayListOf() - protected val commands: MutableList = arrayListOf() - protected val signers: MutableSet = mutableSetOf() +open class TransactionBuilder( + protected val type: TransactionType = TransactionType.General(), + protected val notary: Party? = null, + protected val inputs: MutableList = arrayListOf(), + protected val attachments: MutableList = arrayListOf(), + protected val outputs: MutableList> = arrayListOf(), + protected val commands: MutableList = arrayListOf(), + protected val signers: MutableSet = mutableSetOf()) { val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull() + /** + * Creates a copy of the builder + */ + fun copy(): TransactionBuilder = + TransactionBuilder( + type = type, + notary = notary, + inputs = ArrayList(inputs), + attachments = ArrayList(attachments), + outputs = ArrayList(outputs), + commands = ArrayList(commands), + signers = LinkedHashSet(signers) + ) + /** * Places a [TimestampCommand] in this transaction, removing any existing command if there is one. * The command requires a signature from the Notary service, which acts as a Timestamp Authority. @@ -112,31 +127,32 @@ abstract class TransactionBuilder(protected val type: TransactionType = Transact return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs)) } - open fun addInputState(stateAndRef: StateAndRef<*>) { + open fun addInputState(stateAndRef: StateAndRef<*>) = addInputState(stateAndRef.ref, stateAndRef.state.notary) + + fun addInputState(stateRef: StateRef, notary: Party) { check(currentSigs.isEmpty()) - val notaryKey = stateAndRef.state.notary.owningKey - signers.add(notaryKey) - - inputs.add(stateAndRef.ref) + signers.add(notary.owningKey) + inputs.add(stateRef) } - fun addAttachment(attachment: Attachment) { + fun addAttachment(attachmentId: SecureHash) { check(currentSigs.isEmpty()) - attachments.add(attachment.id) + attachments.add(attachmentId) } - fun addOutputState(state: TransactionState<*>) { + fun addOutputState(state: TransactionState<*>): Int { check(currentSigs.isEmpty()) outputs.add(state) + return outputs.size - 1 } fun addOutputState(state: ContractState, notary: Party) = addOutputState(TransactionState(state, notary)) /** A default notary must be specified during builder construction to use this method */ - fun addOutputState(state: ContractState) { + fun addOutputState(state: ContractState): Int { checkNotNull(notary) { "Need to specify a Notary for the state, or set a default one on TransactionBuilder initialisation" } - addOutputState(state, notary!!) + return addOutputState(state, notary!!) } fun addCommand(arg: Command) { @@ -155,4 +171,4 @@ abstract class TransactionBuilder(protected val type: TransactionType = Transact fun outputStates(): List> = ArrayList(outputs) fun commands(): List = ArrayList(commands) fun attachments(): List = ArrayList(attachments) -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt index d41ad3cd60..d97a92a367 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/LedgerDSLInterpreter.kt @@ -9,8 +9,10 @@ interface OutputStateLookup { } interface LedgerDSLInterpreter> : OutputStateLookup { - fun transaction(transactionLabel: String?, dsl: TransactionDSL.() -> R): WireTransaction - fun unverifiedTransaction(transactionLabel: String?, dsl: TransactionDSL.() -> Unit): WireTransaction + fun _transaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, + dsl: TransactionDSL.() -> R): WireTransaction + fun _unverifiedTransaction(transactionLabel: String?, transactionBuilder: TransactionBuilder, + dsl: TransactionDSL.() -> Unit): WireTransaction fun tweak(dsl: LedgerDSL>.() -> Unit) fun attachment(attachment: InputStream): SecureHash fun verifies() @@ -26,10 +28,14 @@ interface LedgerDSLInterpreter> : Output class LedgerDSL, out L : LedgerDSLInterpreter> (val interpreter: L) : LedgerDSLInterpreter> by interpreter { - fun transaction(dsl: TransactionDSL>.() -> R) = - transaction(null, dsl) - fun unverifiedTransaction(dsl: TransactionDSL>.() -> Unit) = - unverifiedTransaction(null, dsl) + @JvmOverloads + fun transaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), + dsl: TransactionDSL>.() -> R) = + _transaction(label, transactionBuilder, dsl) + @JvmOverloads + fun unverifiedTransaction(label: String? = null, transactionBuilder: TransactionBuilder = TransactionBuilder(), + dsl: TransactionDSL>.() -> Unit) = + _unverifiedTransaction(label, transactionBuilder, dsl) inline fun String.outputStateAndRef(): StateAndRef = retrieveOutputStateAndRef(S::class.java, this) diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt index 564524dc85..6e9967dfd6 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TestDSL.kt @@ -16,11 +16,12 @@ import java.util.* fun transaction( transactionLabel: String? = null, + transactionBuilder: TransactionBuilder = TransactionBuilder(), dsl: TransactionDSL< EnforceVerifyOrFail, TransactionDSLInterpreter >.() -> EnforceVerifyOrFail -) = JavaTestHelpers.transaction(transactionLabel, dsl) +) = JavaTestHelpers.transaction(transactionLabel, transactionBuilder, dsl) fun ledger( identityService: IdentityService = MOCK_IDENTITY_SERVICE, @@ -68,57 +69,55 @@ sealed class EnforceVerifyOrFail { internal object Token: EnforceVerifyOrFail() } +class DuplicateOutputLabel(label: String) : Exception("Output label '$label' already used") + /** * This interpreter builds a transaction, and [TransactionDSL.verifies] that the resolved transaction is correct. Note * that transactions corresponding to input states are not verified. Use [LedgerDSL.verifies] for that. */ -data class TestTransactionDSLInterpreter( +data class TestTransactionDSLInterpreter private constructor( override val ledgerInterpreter: TestLedgerDSLInterpreter, - private val inputStateRefs: ArrayList = arrayListOf(), - internal val outputStates: ArrayList = arrayListOf(), - private val attachments: ArrayList = arrayListOf(), - private val commands: ArrayList = arrayListOf(), - private val signers: LinkedHashSet = LinkedHashSet(), - private val transactionType: TransactionType = TransactionType.General() + val transactionBuilder: TransactionBuilder, + internal val labelToIndexMap: HashMap ) : TransactionDSLInterpreter, OutputStateLookup by ledgerInterpreter { + + constructor( + ledgerInterpreter: TestLedgerDSLInterpreter, + transactionBuilder: TransactionBuilder // = TransactionBuilder() + ) : this(ledgerInterpreter, transactionBuilder, HashMap()) + private fun copy(): TestTransactionDSLInterpreter = TestTransactionDSLInterpreter( ledgerInterpreter = ledgerInterpreter, - inputStateRefs = ArrayList(inputStateRefs), - outputStates = ArrayList(outputStates), - attachments = ArrayList(attachments), - commands = ArrayList(commands), - signers = LinkedHashSet(signers), - transactionType = transactionType + transactionBuilder = transactionBuilder.copy(), + labelToIndexMap = HashMap(labelToIndexMap) ) - internal fun toWireTransaction(): WireTransaction = - WireTransaction( - inputs = inputStateRefs, - outputs = outputStates.map { it.state }, - attachments = attachments, - commands = commands, - signers = signers.toList(), - type = transactionType - ) + internal fun toWireTransaction() = transactionBuilder.toWireTransaction() override fun input(stateRef: StateRef) { val notary = ledgerInterpreter.resolveStateRef(stateRef).notary - signers.add(notary.owningKey) - inputStateRefs.add(stateRef) + transactionBuilder.addInputState(stateRef, notary) } override fun _output(label: String?, notary: Party, contractState: ContractState) { - outputStates.add(LabeledOutput(label, TransactionState(contractState, notary))) + val outputIndex = transactionBuilder.addOutputState(contractState, notary) + if (label != null) { + if (labelToIndexMap.contains(label)) { + throw DuplicateOutputLabel(label) + } else { + labelToIndexMap[label] = outputIndex + } + } } override fun attachment(attachmentId: SecureHash) { - attachments.add(attachmentId) + transactionBuilder.addAttachment(attachmentId) } override fun _command(signers: List, commandData: CommandData) { - this.signers.addAll(signers) - commands.add(Command(commandData, signers)) + val command = Command(commandData, signers) + transactionBuilder.addCommand(command) } override fun verifies(): EnforceVerifyOrFail { @@ -243,9 +242,10 @@ data class TestLedgerDSLInterpreter private constructor ( storageService.attachments.openAttachment(attachmentId) ?: throw AttachmentResolutionException(attachmentId) private fun interpretTransactionDsl( + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Return ): TestTransactionDSLInterpreter { - val transactionInterpreter = TestTransactionDSLInterpreter(this) + val transactionInterpreter = TestTransactionDSLInterpreter(this, transactionBuilder) dsl(TransactionDSL(transactionInterpreter)) return transactionInterpreter } @@ -274,18 +274,20 @@ data class TestLedgerDSLInterpreter private constructor ( private fun recordTransactionWithTransactionMap( transactionLabel: String?, + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> R, transactionMap: HashMap = HashMap() ): WireTransaction { val transactionLocation = getCallerLocation(3) - val transactionInterpreter = interpretTransactionDsl(dsl) + val transactionInterpreter = interpretTransactionDsl(transactionBuilder, dsl) // Create the WireTransaction val wireTransaction = transactionInterpreter.toWireTransaction() // Record the output states - transactionInterpreter.outputStates.forEachIndexed { index, labeledOutput -> - if (labeledOutput.label != null) { - labelToOutputStateAndRefs[labeledOutput.label] = wireTransaction.outRef(index) + transactionInterpreter.labelToIndexMap.forEach { label, index -> + if (labelToOutputStateAndRefs.contains(label)) { + throw DuplicateOutputLabel(label) } + labelToOutputStateAndRefs[label] = wireTransaction.outRef(index) } transactionMap[wireTransaction.serialized.hash] = @@ -294,15 +296,17 @@ data class TestLedgerDSLInterpreter private constructor ( return wireTransaction } - override fun transaction( + override fun _transaction( transactionLabel: String?, + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> EnforceVerifyOrFail - ) = recordTransactionWithTransactionMap(transactionLabel, dsl, transactionWithLocations) + ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, transactionWithLocations) - override fun unverifiedTransaction( + override fun _unverifiedTransaction( transactionLabel: String?, + transactionBuilder: TransactionBuilder, dsl: TransactionDSL.() -> Unit - ) = recordTransactionWithTransactionMap(transactionLabel, dsl, nonVerifiedTransactionWithLocations) + ) = recordTransactionWithTransactionMap(transactionLabel, transactionBuilder, dsl, nonVerifiedTransactionWithLocations) override fun tweak( dsl: LedgerDSL >.() -> EnforceVerifyOrFail - ) = ledger { transaction(transactionLabel, dsl) } + ) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) } } val TEST_TX_TIME = JavaTestHelpers.TEST_TX_TIME diff --git a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt index f8421a8326..7f83f326a9 100644 --- a/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt +++ b/core/src/main/kotlin/com/r3corda/core/testing/TransactionDSLInterpreter.kt @@ -59,7 +59,7 @@ class TransactionDSL> (val interpreter: * Adds the passed in state as a non-verified transaction output to the ledger and adds that as an input. */ fun input(state: ContractState) { - val transaction = ledgerInterpreter.unverifiedTransaction(null) { + val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder()) { output { state } } input(transaction.outRef(0).ref) diff --git a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt index a375fcebfb..8bbfe83ebe 100644 --- a/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/com/r3corda/core/node/AttachmentClassLoaderTests.kt @@ -234,7 +234,7 @@ class AttachmentClassLoaderTests { val attachmentRef = importJar(storage) - tx.addAttachment(storage.openAttachment(attachmentRef)!!) + tx.addAttachment(storage.openAttachment(attachmentRef)!!.id) val wireTransaction = tx.toWireTransaction() @@ -265,7 +265,7 @@ class AttachmentClassLoaderTests { val attachmentRef = importJar(storage) - tx.addAttachment(storage.openAttachment(attachmentRef)!!) + tx.addAttachment(storage.openAttachment(attachmentRef)!!.id) val wireTransaction = tx.toWireTransaction() @@ -280,4 +280,4 @@ class AttachmentClassLoaderTests { } assertEquals(attachmentRef, e.ids.single()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt index d994281a20..fa5a499181 100644 --- a/src/main/kotlin/com/r3corda/demos/TraderDemo.kt +++ b/src/main/kotlin/com/r3corda/demos/TraderDemo.kt @@ -341,7 +341,7 @@ private class TraderDemoProtocolSeller(val otherSide: Party, // TODO: Consider moving these two steps below into generateIssue. // Attach the prospectus. - tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!) + tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) // Requesting timestamping, all CP must be timestamped. tx.setTime(Instant.now(), notaryNode.identity, 30.seconds)