diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/generic/GenericContract.kt b/contracts/src/main/kotlin/com/r3corda/contracts/generic/GenericContract.kt index 7043d316dc..9a49e5c857 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/generic/GenericContract.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/generic/GenericContract.kt @@ -24,7 +24,7 @@ class GenericContract : Contract { // replace parties // must be signed by all parties present in contract before and after command - class Move : TypeOnlyCommandData(), Commands + class Move(val from: Party, val to: Party) : TypeOnlyCommandData(), Commands // must be signed by all parties present in contract class Issue : TypeOnlyCommandData(), Commands @@ -73,10 +73,20 @@ class GenericContract : Contract { is Commands.Issue -> { val outState = tx.outStates.single() as State requireThat { - "the transaction is signed by all involved parties" by ( liableParties(outState.details).all { it in cmd.signers } ) + "the transaction is signed by all liable parties" by ( liableParties(outState.details).all { it in cmd.signers } ) "the transaction has no input states" by tx.inStates.isEmpty() } } + is Commands.Move -> { + val inState = tx.inStates.single() as State + val outState = tx.outStates.single() as State + requireThat { + // todo: + // - check actual state output + "the transaction is signed by all liable parties" by ( liableParties(outState.details).all { it in cmd.signers } ) + "output state does not reflect move command" by (replaceParty(inState.details, value.from, value.to).equals(outState.details)) + } + } else -> throw IllegalArgumentException("Unrecognised command") } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/generic/Kontract.kt b/contracts/src/main/kotlin/com/r3corda/contracts/generic/Kontract.kt index 0c38a8959d..c7c74f372c 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/generic/Kontract.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/generic/Kontract.kt @@ -34,7 +34,7 @@ data class Action(val name: String, val condition: Observable, // only actions can be or'ed together data class Or(val contracts: Set) : Kontract -/** returns list of involved parties for a given contract */ +/** returns list of potentially liable parties for a given contract */ fun liableParties(contract: Kontract) : Set { fun visit(contract: Kontract) : ImmutableSet { @@ -58,6 +58,45 @@ fun liableParties(contract: Kontract) : Set { return visit(contract); } +/** returns list of involved parties for a given contract */ +fun involvedParties(contract: Kontract) : Set { + + fun visit(contract: Kontract) : ImmutableSet { + return when (contract) { + is Zero -> ImmutableSet.of() + is Transfer -> ImmutableSet.of(contract.from.owningKey) + is Action -> Sets.union( visit(contract.kontract), contract.actors.map { it.owningKey }.toSet() ).immutableCopy() + is And -> + contract.kontracts.fold( ImmutableSet.builder(), { builder, k -> builder.addAll( visit(k)) } ).build() + is Or -> + contract.contracts.fold( ImmutableSet.builder(), { builder, k -> builder.addAll( visit(k)) } ).build() + else -> throw IllegalArgumentException() + } + } + + return visit(contract); +} + +fun replaceParty(action: Action, from: Party, to: Party) : Action { + if (action.actors.contains(from)) { + return Action( action.name, action.condition, action.actors - from + to, replaceParty(action.kontract, from, to)) + } + return Action( action.name, action.condition, action.actors, replaceParty(action.kontract, from, to)) +} + +fun replaceParty(contract: Kontract, from: Party, to: Party) : Kontract { + return when (contract) { + is Zero -> contract + is Transfer -> Transfer( contract.amount, contract.currency, + if (contract.from == from) to else contract.from, + if (contract.to == from) to else contract.to ) + is Action -> replaceParty(contract, from, to) + is And -> And( contract.kontracts.map { replaceParty(it, from, to) }.toSet() ) + is Or -> Or( contract.contracts.map { replaceParty(it, from, to) }.toSet() ) + else -> throw IllegalArgumentException() + } +} + fun actions(contract: Kontract) : Map { when (contract) { diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/generic/FXSwap.kt b/contracts/src/test/kotlin/com/r3corda/contracts/generic/FXSwap.kt index fd13a0c229..46eb03b956 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/generic/FXSwap.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/generic/FXSwap.kt @@ -37,11 +37,11 @@ class FXSwap { tweak { arg(roadRunner.owningKey) { GenericContract.Commands.Issue() } - this `fails requirement` "the transaction is signed by all involved parties" + this `fails requirement` "the transaction is signed by all liable parties" } tweak { arg(wileECoyote.owningKey) { GenericContract.Commands.Issue() } - this `fails requirement` "the transaction is signed by all involved parties" + this `fails requirement` "the transaction is signed by all liable parties" } arg(wileECoyote.owningKey, roadRunner.owningKey) { GenericContract.Commands.Issue() } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/generic/ZCB.kt b/contracts/src/test/kotlin/com/r3corda/contracts/generic/ZCB.kt index 3a92f570bf..6a42a6948a 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/generic/ZCB.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/generic/ZCB.kt @@ -17,6 +17,14 @@ class ZCB { } } + + val contractMove = + (porkyPig or wileECoyote).may { + "execute".givenThat(after("01/09/2017")) { + wileECoyote.gives(porkyPig, 100.K*GBP) + } + } + val transfer = kontract { wileECoyote.gives(roadRunner, 100.K*GBP) } val transferWrong = kontract { wileECoyote.gives(roadRunner, 80.K*GBP) } @@ -25,6 +33,8 @@ class ZCB { val outState = GenericContract.State( DUMMY_NOTARY, transfer ) val outStateWrong = GenericContract.State( DUMMY_NOTARY, transferWrong ) + val outStateMove = GenericContract.State( DUMMY_NOTARY, contractMove ) + @Test fun basic() { assert( Zero().equals(Zero())) @@ -41,7 +51,7 @@ class ZCB { tweak { arg(roadRunner.owningKey) { GenericContract.Commands.Issue() } - this `fails requirement` "the transaction is signed by all involved parties" + this `fails requirement` "the transaction is signed by all liable parties" } arg(wileECoyote.owningKey) { GenericContract.Commands.Issue() } @@ -89,4 +99,34 @@ class ZCB { } } + @Test + fun move() { + transaction { + input { inState } + + tweak { + output { outStateMove } + arg(roadRunner.owningKey) { + GenericContract.Commands.Move(roadRunner, porkyPig) + } + this `fails requirement` "the transaction is signed by all liable parties" + } + + tweak { + output { inState } + arg(roadRunner.owningKey, porkyPig.owningKey, wileECoyote.owningKey) { + GenericContract.Commands.Move(roadRunner, porkyPig) + } + this `fails requirement` "output state does not reflect move command" + } + + output { outStateMove} + + arg(roadRunner.owningKey, porkyPig.owningKey, wileECoyote.owningKey) { + GenericContract.Commands.Move(roadRunner, porkyPig) + } + this.accepts() + } + } + } \ No newline at end of file