0
votes

I have a use case where in one atomic transaction I would like to transition two or more states of same type to different lifecycle. ( i.e. )

val x : SomeState
val y : SomeState
  • x should transition to Approved with Command.Approve
  • y should transition to Audited with Command.Audit

How can the contract code run the verifyApprove only on x while running verifyAudit on y. There can be an infinite numbers of SomeState that can transition to different lifecycles in the same atomic transaction.

Do I have to make Command.Approve have a constructor with a List<UniqueIdentitifer> so it can determine on which states to run its corresponding verify function. Subsequently populate the command in the flow?

4

4 Answers

1
votes

Every verify function verifies a transaction in its entirety, and not its individual states. However, you can easily write logic within verify to control how each state type is checked. For example, if the XState and YState states share the same contract, you could do:

class XAndYContract: Contract {
    override fun verify(tx: LedgerTransaction) {
        val inputXStates = tx.inputsOfType<XState>()
        val outputXStates = tx.outputsOfType<XState>()
        val inputYStates = tx.inputsOfType<YState>()
        val outputYStates = tx.outputsOfType<YState>()

        requireThat {
            "All XState inputs are unapproved." using (inputXStates.all { it.approved == false })
            "All XState outputs are approved" using (outputXStates.all { it.approved == true })
            "All YState inputs are unaudited." using (inputYStates.all { it.audited == false })
            "All YState outputs are audited" using (outputYStates.all { it.audited == true })
        }
    }
}

This is just an example. You have the full capabilities of Java/Kotlin at your disposal to control which states are checked and how.

Another option would be to split the logic into two contracts - XContract and YContract. Then you would have:

class XContract : Contract {
    override fun verify(tx: LedgerTransaction) {
        val inputXStates = tx.inputsOfType<XState>()
        val outputXStates = tx.outputsOfType<XState>()

        requireThat {
            "All XState inputs are unapproved." using (inputXStates.all { it.approved == false })
            "All XState outputs are approved" using (outputXStates.all { it.approved == true })
        }
    }
}

And:

class YContract : Contract {
    override fun verify(tx: LedgerTransaction) {
        val inputYStates = tx.inputsOfType<YState>()
        val outputYStates = tx.outputsOfType<YState>()

        requireThat {
            "All YState inputs are unaudited." using (inputYStates.all { it.audited == false })
            "All YState outputs are audited" using (outputYStates.all { it.audited == true })
        }
    }
}

Edit: The examples above assume there are two types of states. Suppose there is only one state type:

class MyState(val lifecycle: Lifecycle, override val linearId: UniqueIdentifier) : LinearState {
    override val participants = listOf<AbstractParty>()
}

Where Lifecycle is defined as:

enum class Lifecycle {
    ISSUED, AUDITED, APPROVED
}

And we want to impose the rule that in each transaction, any inputs in the ISSUED stage are transitioned to APPROVED, and any inputs in the APPROVED stage are transitioned to AUDITED.

We could achieve this using groupStates:

class MyContract : Contract {
    override fun verify(tx: LedgerTransaction) {
        val groups = tx.groupStates { it: MyState -> it.linearId }

        requireThat {
            for (group in groups) {
                val inputState = group.inputs.single()
                val outputState = group.outputs.single()
                when (inputState.lifecycle) {
                    Lifecycle.ISSUED -> {
                        "Issued states have been transitioned to approved" using (outputState.lifecycle == Lifecycle.APPROVED)
                    }
                    Lifecycle.APPROVED -> {
                        "Approved states have been transitioned to audited" using (outputState.lifecycle == Lifecycle.AUDITED)
                    }
                }
            }
        }
    }
}

groupStates works by grouping inputs and outputs together based on some rule. In our case, we are grouping inputs and outputs that share the same uniqueIdentifier. We can then check that each input has evolved into the corresponding output correctly.

0
votes

To implement that functionality you just have to use use a switch statement on the command. For example, in verify(tx) you could do something like this:

when (command.value) {
        is Commands.Approve -> verifyApprove(tx, setOfSigners)
        is Commands.Audit -> verifyAudit(tx, setOfSigners)
        else -> throw IllegalArgumentException("Unrecognised command")
}

while the commands could be defined as follows:

interface Commands : CommandData {
    class Issue : TypeOnlyCommandData(), Commands
    class Transfer : TypeOnlyCommandData(), Commands
}

and in verifyApprove() and verifyAudit(), you can do your verification.

0
votes

I tried passing states into the command itself so that we know command needs to take care what states. In verify method then don't check for single command.

interface Commands : CommandData {
    class Create(val outputStates:List<MyState>): TypeOnlyCommandData(), Commands
    class Update(val inOutStates:List<InOutState<MyState>>): TypeOnlyCommandData(), Commands
   }
 ................
@CordaSerializable
data class InOutState<T: ContractState>(val inputState: T, val outputState:T)
 ............
 tx.commands.forEach {

            when (it.value) {
                is Commands.Create -> {
                   val cmd = it.value as Commands.Create

                    requireThat{

                        "Output states must be present" using (cmd.outputStates.isNotEmpty())
                    }
             }
      }
0
votes

One more pattern is to set the Corda contract Command into the State Object and do the grouping by the Command during time of contract verification

@CordaSerializable
interface CommandAwareState<T:CommandData>: LinearState,QueryableState{

     val command: T
}

============
interface LinearStateCommands<T : LinearState> : CommandData {


    fun verify(inOutStates: List<InOutState<T>>, tx: LedgerTransaction)
}
===========


data class StateA(val attribX: String, val attribY: String
                 , override val command: StateAContract.Commands = 
                  StateAContract.Commands.None()): 
                       CommandAwareState<StateAContract.Commands>
 ============

 open class StateAContract : Contract {
companion object {
    @JvmStatic
    val CONTRACT_ID = "com.myproj.contract.StateAContract"
}

interface Commands : LinearStateCommands<DocumentState> {

    class None(): TypeOnlyCommandData(), Commands{
        override fun verify(inOutState: List<InOutState<StateA>>, tx: 
                         LedgerTransaction) {

                requireThat {
                    "There should not be any Input and Outputs" using 
                     (inOutState.isEmpty())
                }

        }


    }

     class Create(): TypeOnlyCommandData(), Commands{

          override fun verify(inOutStates: List<InOutState<DocumentState>>, tx: 
       LedgerTransaction) {
        //WRITE Contract verification
        }  
     }

override fun verify(tx: LedgerTransaction) {

           tx.matchLinearStatesByCommand(DocumentState::class.java).forEach{

               it.key.verify(it.value,tx)
           }
    }
 }
==========

inline fun <T,reified  K : CommandAwareState<T>> LedgerTransaction.matchLinearStatesByCommand(ofType: Class<K>): Map<T,List<InOutState<K>>>{



    groupStates<K,UniqueIdentifier> { it.linearId }.let {

        var mapByCommand = mutableMapOf<T,MutableList<InOutState<K>>>()
        it.forEach {

            if(mapByCommand.containsKey(it.outputs.single().command)){

                mapByCommand.get(it.outputs.single().command)?.add(InOutState(it.inputs.noneOrSingle(),it.outputs.noneOrSingle()))

            }else{

                mapByCommand.put(it.outputs.single().command, mutableListOf(InOutState(it.inputs.noneOrSingle(),it.outputs.noneOrSingle())))

            }


        }
        return mapByCommand
    }

}