Generified Command so that it's more easier to use when querying from LedgerTransaction

This commit is contained in:
Shams Asari 2017-07-24 12:41:21 +01:00
parent d2eb5507f9
commit 800f710fbb
19 changed files with 70 additions and 62 deletions

View File

@ -284,14 +284,13 @@ abstract class TypeOnlyCommandData : CommandData {
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
@CordaSerializable
// DOCSTART 9
data class Command(val value: CommandData, val signers: List<PublicKey>) {
// DOCEND 9
data class Command<T : CommandData>(val value: T, val signers: List<PublicKey>) {
// TODO Introduce NonEmptyList?
init {
require(signers.isNotEmpty())
}
constructor(data: CommandData, key: PublicKey) : this(data, listOf(key))
constructor(data: T, key: PublicKey) : this(data, listOf(key))
private fun commandDataToString() = value.toString().let { if (it.contains("@")) it.replace('$', '.').split("@")[0] else it }
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"

View File

@ -24,12 +24,15 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
@JvmStatic
fun verify(tx: LedgerTransaction) {
// Contract Upgrade transaction should have 1 input, 1 output and 1 command.
verify(tx.inputStates.single(), tx.outputStates.single(), tx.commands.map { Command(it.value, it.signers) }.single())
verify(
tx.inputStates.single(),
tx.outputStates.single(),
tx.commandsOfType<UpgradeCommand>().single())
}
@JvmStatic
fun verify(input: ContractState, output: ContractState, commandData: Command) {
val command = commandData.value as UpgradeCommand
fun verify(input: ContractState, output: ContractState, commandData: Command<UpgradeCommand>) {
val command = commandData.value
val participantKeys: Set<PublicKey> = input.participants.map { it.owningKey }.toSet()
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
@Suppress("UNCHECKED_CAST")

View File

@ -265,7 +265,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
// Otherwise we just assume the code we need is on the classpath already.
kryo.useClassLoader(attachmentsClassLoader(kryo, attachmentHashes) ?: javaClass.classLoader) {
val outputs = kryo.readClassAndObject(input) as List<TransactionState<ContractState>>
val commands = kryo.readClassAndObject(input) as List<Command>
val commands = kryo.readClassAndObject(input) as List<Command<*>>
val notary = kryo.readClassAndObject(input) as Party?
val signers = kryo.readClassAndObject(input) as List<PublicKey>
val transactionType = kryo.readClassAndObject(input) as TransactionType

View File

@ -247,7 +247,8 @@ class LedgerTransaction(
* @param index the position of the item in the commands.
* @return The Command at the requested index
*/
fun getCommand(index: Int): Command = Command(commands[index].value, commands[index].signers)
@Suppress("UNCHECKED_CAST")
fun <T : CommandData> getCommand(index: Int): Command<T> = Command(commands[index].value as T, commands[index].signers)
/**
* Helper to simplify getting all [Command] items with a [CommandData] of a particular class, interface, or base class.
@ -255,11 +256,11 @@ class LedgerTransaction(
* [clazz] must be an extension of [CommandData].
* @return the possibly empty list of commands with [CommandData] values matching the clazz restriction.
*/
fun <T : CommandData> commandsOfType(clazz: Class<T>): List<Command> {
fun <T : CommandData> commandsOfType(clazz: Class<T>): List<Command<T>> {
return commands.mapNotNull { (signers, _, value) -> clazz.castIfPossible(value)?.let { Command(it, signers) } }
}
inline fun <reified T : CommandData> commandsOfType(): List<Command> = commandsOfType(T::class.java)
inline fun <reified T : CommandData> commandsOfType(): List<Command<T>> = commandsOfType(T::class.java)
/**
* Helper to simplify filtering [Command] items according to a [Predicate].
@ -269,12 +270,11 @@ class LedgerTransaction(
* The class filtering is applied before the predicate.
* @return the possibly empty list of [Command] items with [CommandData] values matching the predicate and clazz restrictions.
*/
fun <T : CommandData> filterCommands(clazz: Class<T>, predicate: Predicate<T>): List<Command> {
@Suppress("UNCHECKED_CAST")
return commandsOfType(clazz).filter { predicate.test(it.value as T) }
fun <T : CommandData> filterCommands(clazz: Class<T>, predicate: Predicate<T>): List<Command<T>> {
return commandsOfType(clazz).filter { predicate.test(it.value) }
}
inline fun <reified T : CommandData> filterCommands(crossinline predicate: (T) -> Boolean): List<Command> {
inline fun <reified T : CommandData> filterCommands(crossinline predicate: (T) -> Boolean): List<Command<T>> {
return filterCommands(T::class.java, Predicate { predicate(it) })
}
@ -287,12 +287,11 @@ class LedgerTransaction(
* @return the [Command] item with [CommandData] values matching the predicate and clazz restrictions.
* @throws IllegalArgumentException if no items, or multiple items matched the requirements.
*/
fun <T : CommandData> findCommand(clazz: Class<T>, predicate: Predicate<T>): Command {
@Suppress("UNCHECKED_CAST")
return commandsOfType(clazz).single { predicate.test(it.value as T) }
fun <T : CommandData> findCommand(clazz: Class<T>, predicate: Predicate<T>): Command<T> {
return commandsOfType(clazz).single { predicate.test(it.value) }
}
inline fun <reified T : CommandData> findCommand(crossinline predicate: (T) -> Boolean): Command {
inline fun <reified T : CommandData> findCommand(crossinline predicate: (T) -> Boolean): Command<T> {
return findCommand(T::class.java, Predicate { predicate(it) })
}

View File

@ -29,7 +29,7 @@ interface TraversableTransaction {
val inputs: List<StateRef>
val attachments: List<SecureHash>
val outputs: List<TransactionState<ContractState>>
val commands: List<Command>
val commands: List<Command<*>>
val notary: Party?
val mustSign: List<PublicKey>
val type: TransactionType?
@ -77,7 +77,7 @@ class FilteredLeaves(
override val inputs: List<StateRef>,
override val attachments: List<SecureHash>,
override val outputs: List<TransactionState<ContractState>>,
override val commands: List<Command>,
override val commands: List<Command<*>>,
override val notary: Party?,
override val mustSign: List<PublicKey>,
override val type: TransactionType?,

View File

@ -37,7 +37,7 @@ open class TransactionBuilder(
protected val inputs: MutableList<StateRef> = arrayListOf(),
protected val attachments: MutableList<SecureHash> = arrayListOf(),
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command> = arrayListOf(),
protected val commands: MutableList<Command<*>> = arrayListOf(),
protected val signers: MutableSet<PublicKey> = mutableSetOf(),
protected var window: TimeWindow? = null) {
constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID())
@ -65,7 +65,7 @@ open class TransactionBuilder(
is SecureHash -> addAttachment(t)
is TransactionState<*> -> addOutputState(t)
is ContractState -> addOutputState(t)
is Command -> addCommand(t)
is Command<*> -> addCommand(t)
is CommandData -> throw IllegalArgumentException("You passed an instance of CommandData, but that lacks the pubkey. You need to wrap it in a Command object first.")
is TimeWindow -> setTimeWindow(t)
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
@ -105,7 +105,9 @@ open class TransactionBuilder(
}
@JvmOverloads
fun addOutputState(state: ContractState, notary: Party, encumbrance: Int? = null) = addOutputState(TransactionState(state, notary, encumbrance))
fun addOutputState(state: ContractState, notary: Party, encumbrance: Int? = null): TransactionBuilder {
return addOutputState(TransactionState(state, notary, encumbrance))
}
/** A default notary must be specified during builder construction to use this method */
fun addOutputState(state: ContractState): TransactionBuilder {
@ -114,7 +116,7 @@ open class TransactionBuilder(
return this
}
fun addCommand(arg: Command): TransactionBuilder {
fun addCommand(arg: Command<*>): TransactionBuilder {
// TODO: replace pubkeys in commands with 'pointers' to keys in signers
signers.addAll(arg.signers)
commands.add(arg)
@ -149,7 +151,7 @@ open class TransactionBuilder(
fun inputStates(): List<StateRef> = ArrayList(inputs)
fun attachments(): List<SecureHash> = ArrayList(attachments)
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
fun commands(): List<Command> = ArrayList(commands)
fun commands(): List<Command<*>> = ArrayList(commands)
/** The signatures that have been collected so far - might be incomplete! */
@Deprecated("Signatures should be gathered on a SignedTransaction instead.")

View File

@ -24,7 +24,7 @@ class WireTransaction(
override val attachments: List<SecureHash>,
outputs: List<TransactionState<ContractState>>,
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
override val commands: List<Command>,
override val commands: List<Command<*>>,
notary: Party?,
signers: List<PublicKey>,
type: TransactionType,
@ -125,7 +125,7 @@ class WireTransaction(
val buf = StringBuilder()
buf.appendln("Transaction:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: ${output.data}")
for ((data) in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $data")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString()

View File

@ -112,10 +112,10 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
@Test
fun `Simple Command Indexer tests`() {
val ltx = makeDummyTransaction()
assertEquals(0, (ltx.getCommand(0).value as Commands.Cmd1).id)
assertEquals(0, (ltx.getCommand(1).value as Commands.Cmd2).id)
assertEquals(3, (ltx.getCommand(6).value as Commands.Cmd1).id)
assertEquals(3, (ltx.getCommand(7).value as Commands.Cmd2).id)
assertEquals(0, ltx.getCommand<Commands.Cmd1>(0).value.id)
assertEquals(0, ltx.getCommand<Commands.Cmd2>(1).value.id)
assertEquals(3, ltx.getCommand<Commands.Cmd1>(6).value.id)
assertEquals(3, ltx.getCommand<Commands.Cmd2>(7).value.id)
assertFailsWith<IndexOutOfBoundsException> { ltx.getOutput(10) }
}
@ -178,10 +178,10 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
val ltx = makeDummyTransaction()
val intCmd1 = ltx.commandsOfType(Commands.Cmd1::class.java)
assertEquals(5, intCmd1.size)
assertEquals(listOf(0, 1, 2, 3, 4), intCmd1.map { (it.value as Commands.Cmd1).id })
assertEquals(listOf(0, 1, 2, 3, 4), intCmd1.map { it.value.id })
val intCmd2 = ltx.commandsOfType<Commands.Cmd2>()
assertEquals(5, intCmd2.size)
assertEquals(listOf(0, 1, 2, 3, 4), intCmd2.map { (it.value as Commands.Cmd2).id })
assertEquals(listOf(0, 1, 2, 3, 4), intCmd2.map { it.value.id })
val notPresentQuery = ltx.commandsOfType(FungibleAsset.Commands.Exit::class.java)
assertEquals(emptyList(), notPresentQuery)
}
@ -237,9 +237,9 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
val ltx = makeDummyTransaction()
val intCmds1 = ltx.filterCommands(Commands.Cmd1::class.java, Predicate { it.id.rem(2) == 0 })
assertEquals(3, intCmds1.size)
assertEquals(listOf(0, 2, 4), intCmds1.map { (it.value as Commands.Cmd1).id })
assertEquals(listOf(0, 2, 4), intCmds1.map { it.value.id })
val intCmds2 = ltx.filterCommands<Commands.Cmd2> { it.id == 3 }
assertEquals(3, (intCmds2.single().value as Commands.Cmd2).id)
assertEquals(3, intCmds2.single().value.id)
}
@Test

View File

@ -102,7 +102,7 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
return when (elem) {
is StateRef -> true
is TransactionState<*> -> elem.data.participants[0].owningKey.keys == MINI_CORP_PUBKEY.keys
is Command -> MEGA_CORP_PUBKEY in elem.signers
is Command<*> -> MEGA_CORP_PUBKEY in elem.signers
is TimeWindow -> true
is PublicKey -> elem == MEGA_CORP_PUBKEY
else -> false

View File

@ -43,7 +43,9 @@ UNRELEASED
``LedgerTransaction`` as passed in parameter. The prinicpal consequence of this is that the types of the input and output
collections on the transaction object have changed, so it may be necessary to ``map`` down to the ``ContractState``
sub-properties in existing code.
It is intended that new helper methods will be added shortly to the API to reduce the impact of these changes.
* Added various query methods to ``LedgerTransaction`` to simplify querying of states and commands. In the same vain
``Command`` is now parameterised on the ``CommandData`` field.
Milestone 13
------------

View File

@ -263,11 +263,11 @@ public class FlowCookbookJava {
// public keys. To be valid, the transaction requires a signature
// matching every public key in all of the transaction's commands.
// DOCSTART 24
CommandData commandData = new DummyContract.Commands.Create();
DummyContract.Commands.Create commandData = new DummyContract.Commands.Create();
PublicKey ourPubKey = getServiceHub().getLegalIdentityKey();
PublicKey counterpartyPubKey = counterparty.getOwningKey();
List<PublicKey> requiredSigners = ImmutableList.of(ourPubKey, counterpartyPubKey);
Command ourCommand = new Command(commandData, requiredSigners);
Command<DummyContract.Commands.Create> ourCommand = new Command<>(commandData, requiredSigners);
// DOCEND 24
// ``CommandData`` can either be:

View File

@ -12,7 +12,10 @@ import net.corda.core.node.services.ServiceType
import net.corda.core.node.services.Vault.Page
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.transactions.*
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.ProgressTracker.Step
import net.corda.core.utilities.UntrustworthyData
@ -243,11 +246,11 @@ object FlowCookbook {
// public keys. To be valid, the transaction requires a signature
// matching every public key in all of the transaction's commands.
// DOCSTART 24
val commandData: CommandData = DummyContract.Commands.Create()
val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create()
val ourPubKey: PublicKey = serviceHub.legalIdentityKey
val counterpartyPubKey: PublicKey = counterparty.owningKey
val requiredSigners: List<PublicKey> = listOf(ourPubKey, counterpartyPubKey)
val ourCommand: Command = Command(commandData, requiredSigners)
val ourCommand: Command<DummyContract.Commands.Create> = Command(commandData, requiredSigners)
// DOCEND 24
// ``CommandData`` can either be:

View File

@ -1,4 +1,4 @@
package net.corda.contracts.isolated
package net.corda.contracts
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash

View File

@ -56,8 +56,8 @@ class CommandDataGenerator : Generator<CommandData>(CommandData::class.java) {
}
}
class CommandGenerator : Generator<Command>(Command::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Command {
class CommandGenerator : Generator<Command<*>>(Command::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Command<*> {
val signersGenerator = ArrayListGenerator()
signersGenerator.addComponentGenerators(listOf(PublicKeyGenerator()))
return Command(CommandDataGenerator().generate(random, status), PublicKeyGenerator().generate(random, status))

View File

@ -1,10 +1,7 @@
package net.corda.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.TransactionType
import net.corda.core.contracts.UpgradedContract
import net.corda.core.contracts.requireThat
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.*
import net.corda.core.identity.AnonymousPartyAndPath
@ -121,7 +118,10 @@ class ContractUpgradeHandler(otherSide: Party) : AbstractStateReplacementFlow.Ac
"The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification == authorisedUpgrade)
"The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx)
}
ContractUpgradeFlow.verify(oldStateAndRef.state.data, expectedTx.outRef<ContractState>(0).state.data, expectedTx.commands.single())
ContractUpgradeFlow.verify(
oldStateAndRef.state.data,
expectedTx.outRef<ContractState>(0).state.data,
expectedTx.toLedgerTransaction(serviceHub).commandsOfType<UpgradeCommand>().single())
}
}

View File

@ -8,7 +8,6 @@ import net.corda.contracts.Tenor
import net.corda.contracts.math.CubicSplineInterpolator
import net.corda.contracts.math.Interpolator
import net.corda.contracts.math.InterpolatorFactory
import net.corda.core.internal.ThreadBox
import net.corda.core.contracts.Command
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.MerkleTreeException
@ -18,6 +17,7 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.internal.ThreadBox
import net.corda.core.node.PluginServiceHub
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.CordaService
@ -151,7 +151,7 @@ object NodeInterestRates {
throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.")
}
// Performing validation of obtained FilteredLeaves.
fun commandValidator(elem: Command): Boolean {
fun commandValidator(elem: Command<*>): Boolean {
if (!(identity.owningKey in elem.signers && elem.value is Fix))
throw IllegalArgumentException("Oracle received unknown command (not in signers or not Fix).")
val fix = elem.value as Fix
@ -163,7 +163,7 @@ object NodeInterestRates {
fun check(elem: Any): Boolean {
return when (elem) {
is Command -> commandValidator(elem)
is Command<*> -> commandValidator(elem)
else -> throw IllegalArgumentException("Oracle received data of different type than expected.")
}
}

View File

@ -80,7 +80,7 @@ object FixingFlow {
@Suspendable
override fun filtering(elem: Any): Boolean {
return when (elem) {
is Command -> oracleParty.owningKey in elem.signers && elem.value is Fix
is Command<*> -> oracleParty.owningKey in elem.signers && elem.value is Fix
else -> false
}
}

View File

@ -51,12 +51,12 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() {
fun fixCmdFilter(elem: Any): Boolean {
return when (elem) {
is Command -> oracle.identity.owningKey in elem.signers && elem.value is Fix
is Command<*> -> oracle.identity.owningKey in elem.signers && elem.value is Fix
else -> false
}
}
fun filterCmds(elem: Any): Boolean = elem is Command
fun filterCmds(elem: Any): Boolean = elem is Command<*>
@Before
fun setUp() {
@ -179,7 +179,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() {
val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first()
fun filtering(elem: Any): Boolean {
return when (elem) {
is Command -> oracle.identity.owningKey in elem.signers && elem.value is Fix
is Command<*> -> oracle.identity.owningKey in elem.signers && elem.value is Fix
is TransactionState<ContractState> -> true
else -> false
}
@ -234,7 +234,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() {
: RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) {
override fun filtering(elem: Any): Boolean {
return when (elem) {
is Command -> oracle.owningKey in elem.signers && elem.value is Fix
is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix
else -> false
}
}

View File

@ -2,7 +2,6 @@ package net.corda.verifier
import net.corda.client.mock.*
import net.corda.core.contracts.*
import net.corda.testing.contracts.DummyContract
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.entropyToKeyPair
@ -12,6 +11,7 @@ import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.testing.contracts.DummyContract
import java.io.ByteArrayInputStream
import java.math.BigInteger
import java.security.PublicKey
@ -49,7 +49,7 @@ data class GeneratedLedger(
Generator.replicatePoisson(1.0, pickOneOrMaybeNew(attachments, attachmentGenerator))
}
val commandsGenerator: Generator<List<Pair<Command, Party>>> by lazy {
val commandsGenerator: Generator<List<Pair<Command<*>, Party>>> by lazy {
Generator.replicatePoisson(4.0, commandGenerator(identities))
}
@ -214,7 +214,7 @@ val stateGenerator: Generator<ContractState> =
GeneratedState(nonce, participants.map { AnonymousParty(it) })
}
fun commandGenerator(partiesToPickFrom: Collection<Party>): Generator<Pair<Command, Party>> {
fun commandGenerator(partiesToPickFrom: Collection<Party>): Generator<Pair<Command<*>, Party>> {
return pickOneOrMaybeNew(partiesToPickFrom, partyGenerator).combine(Generator.long()) { signer, nonce ->
Pair(
Command(GeneratedCommandData(nonce), signer.owningKey),