mirror of
https://github.com/corda/corda.git
synced 2025-01-21 03:55:00 +00:00
Contracts: a bit more work on the CP implementation, add a unit test for redemption at time.
This commit is contained in:
parent
a7bfff486a
commit
0b02f64f3d
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
TODO
|
||||||
|
|
||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
|
||||||
.gradle
|
.gradle
|
||||||
|
@ -50,17 +50,23 @@ object ComedyPaper : Contract {
|
|||||||
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
|
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
|
||||||
val input = inStates.filterIsInstance<ComedyPaper.State>().single()
|
val input = inStates.filterIsInstance<ComedyPaper.State>().single()
|
||||||
|
|
||||||
|
requireThat {
|
||||||
|
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
|
||||||
|
}
|
||||||
|
|
||||||
when (command.value) {
|
when (command.value) {
|
||||||
is Commands.Move -> requireThat {
|
is Commands.Move -> requireThat {
|
||||||
val output = outStates.filterIsInstance<ComedyPaper.State>().single()
|
val output = outStates.filterIsInstance<ComedyPaper.State>().single()
|
||||||
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
|
|
||||||
"the output state is the same as the input state except for owner" by (input.withoutOwner() == output.withoutOwner())
|
"the output state is the same as the input state except for owner" by (input.withoutOwner() == output.withoutOwner())
|
||||||
}
|
}
|
||||||
|
|
||||||
is Commands.Redeem -> requireThat {
|
is Commands.Redeem -> requireThat {
|
||||||
val received = outStates.sumCash()
|
val received = try {
|
||||||
|
outStates.sumCash()
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
throw IllegalStateException("invalid cash outputs")
|
||||||
|
}
|
||||||
// Do we need to check the signature of the issuer here too?
|
// Do we need to check the signature of the issuer here too?
|
||||||
"the transaction is signed by the owner of the CP" by (command.signers.contains(input.owner))
|
|
||||||
"the paper must have matured" by (input.maturityDate < time)
|
"the paper must have matured" by (input.maturityDate < time)
|
||||||
"the received amount equals the face value" by (received == input.faceValue)
|
"the received amount equals the face value" by (received == input.faceValue)
|
||||||
"the paper must be destroyed" by outStates.filterIsInstance<ComedyPaper.State>().none()
|
"the paper must be destroyed" by outStates.filterIsInstance<ComedyPaper.State>().none()
|
||||||
|
@ -26,7 +26,7 @@ inline fun <reified T : Command> List<VerifiedSigned<Command>>.requireSingleComm
|
|||||||
select<T>().single()
|
select<T>().single()
|
||||||
} catch (e: NoSuchElementException) {
|
} catch (e: NoSuchElementException) {
|
||||||
// Better error message.
|
// Better error message.
|
||||||
throw IllegalStateException("Required ${T::class.simpleName} command")
|
throw IllegalStateException("Required ${T::class.qualifiedName} command")
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
@ -51,13 +51,14 @@ val Double.SWISS_FRANCS: Amount get() = Amount((this * 100).toInt(), USD)
|
|||||||
// region Requirements
|
// region Requirements
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
object Requirements {
|
class Requirements {
|
||||||
infix fun String.by(expr: Boolean) {
|
infix fun String.by(expr: Boolean) {
|
||||||
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
|
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val R = Requirements()
|
||||||
inline fun requireThat(body: Requirements.() -> Unit) {
|
inline fun requireThat(body: Requirements.() -> Unit) {
|
||||||
Requirements.body()
|
R.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
@ -60,43 +60,48 @@ val TEST_PROGRAM_MAP: Map<SecureHash, Contract> = mapOf(
|
|||||||
// TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block
|
// TODO: Make it impossible to forget to test either a failure or an accept for each transaction{} block
|
||||||
|
|
||||||
// Corresponds to the args to Contract.verify
|
// Corresponds to the args to Contract.verify
|
||||||
data class TransactionForTest(
|
class TransactionForTest() {
|
||||||
private val inStates: MutableList<ContractState> = arrayListOf(),
|
private val inStates = arrayListOf<ContractState>()
|
||||||
private val outStates: MutableList<ContractState> = arrayListOf(),
|
|
||||||
|
class LabeledOutput(val label: String?, val state: ContractState) {
|
||||||
|
override fun toString() = state.toString() + (if (label != null) " ($label)" else "")
|
||||||
|
override fun equals(other: Any?) = other is LabeledOutput && state.equals(other.state)
|
||||||
|
override fun hashCode(): Int = state.hashCode()
|
||||||
|
}
|
||||||
|
private val outStates = arrayListOf<LabeledOutput>()
|
||||||
private val args: MutableList<VerifiedSigned<Command>> = arrayListOf()
|
private val args: MutableList<VerifiedSigned<Command>> = arrayListOf()
|
||||||
) {
|
|
||||||
fun input(s: () -> ContractState) = inStates.add(s())
|
|
||||||
fun output(s: () -> ContractState) = outStates.add(s())
|
|
||||||
fun arg(key: PublicKey, c: () -> Command) = args.add(VerifiedSigned(listOf(key), TEST_KEYS_TO_CORP_MAP[key].let { if (it != null) listOf(it) else emptyList() }, c()))
|
|
||||||
|
|
||||||
private fun run() = TransactionForVerification(inStates, outStates, args, TEST_TX_TIME).verify(TEST_PROGRAM_MAP)
|
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>) : this() {
|
||||||
|
this.inStates.addAll(inStates)
|
||||||
infix fun `fails requirement`(msg: String) {
|
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
|
||||||
try {
|
this.args.addAll(args)
|
||||||
run()
|
|
||||||
} catch(e: Exception) {
|
|
||||||
val m = e.message
|
|
||||||
if (m == null)
|
|
||||||
fail("Threw exception without a message")
|
|
||||||
else
|
|
||||||
if (!m.toLowerCase().contains(msg.toLowerCase())) throw AssertionError("Error was actually: $m", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun input(s: () -> ContractState) = inStates.add(s())
|
||||||
|
fun output(label: String? = null, s: () -> ContractState) = outStates.add(LabeledOutput(label, s()))
|
||||||
|
fun arg(vararg key: PublicKey, c: () -> Command) {
|
||||||
|
val keys = listOf(*key)
|
||||||
|
// TODO: replace map->filterNotNull once upgraded to next Kotlin
|
||||||
|
args.add(VerifiedSigned(keys, keys.map { TEST_KEYS_TO_CORP_MAP[it] }.filterNotNull(), c()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun run(time: Instant) = TransactionForVerification(inStates, outStates.map { it.state }, args, time).verify(TEST_PROGRAM_MAP)
|
||||||
|
|
||||||
|
infix fun `fails requirement`(msg: String) = rejects(msg)
|
||||||
// which is uglier?? :)
|
// which is uglier?? :)
|
||||||
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||||
|
|
||||||
fun accepts() = run()
|
fun accepts(time: Instant = TEST_TX_TIME) = run(time)
|
||||||
fun rejects(withMessage: String? = null) {
|
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
|
||||||
val r = try {
|
val r = try {
|
||||||
run()
|
run(time)
|
||||||
false
|
false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val m = e.message
|
val m = e.message
|
||||||
if (m == null)
|
if (m == null)
|
||||||
fail("Threw exception without a message")
|
fail("Threw exception without a message")
|
||||||
else
|
else
|
||||||
if (withMessage != null && !m.contains(withMessage)) throw AssertionError("Error was actually: $m", e)
|
if (withMessage != null && !m.toLowerCase().contains(withMessage.toLowerCase())) throw AssertionError("Error was actually: $m", e)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
if (!r) throw AssertionError("Expected exception but didn't get one")
|
if (!r) throw AssertionError("Expected exception but didn't get one")
|
||||||
@ -112,6 +117,21 @@ data class TransactionForTest(
|
|||||||
return tx
|
return tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use this to create transactions where the output of this transaction is automatically used as an input of
|
||||||
|
// the next.
|
||||||
|
fun chain(vararg outputLabels: String, body: TransactionForTest.() -> Unit) {
|
||||||
|
val states = outStates.filter {
|
||||||
|
val l = it.label
|
||||||
|
if (l != null)
|
||||||
|
outputLabels.contains(l)
|
||||||
|
else
|
||||||
|
false
|
||||||
|
}.map { it.state } // TODO: replace with mapNotNull after next Kotlin upgrade
|
||||||
|
val tx = TransactionForTest()
|
||||||
|
tx.inStates.addAll(states)
|
||||||
|
tx.body()
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return """transaction {
|
return """transaction {
|
||||||
inputs: $inStates
|
inputs: $inStates
|
||||||
@ -119,6 +139,15 @@ data class TransactionForTest(
|
|||||||
args: $args
|
args: $args
|
||||||
}"""
|
}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?) = this === other || (other is TransactionForTest && inStates == other.inStates && outStates == other.outStates && args == other.args)
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = inStates.hashCode()
|
||||||
|
result += 31 * result + outStates.hashCode()
|
||||||
|
result += 31 * result + args.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
|
fun transaction(body: TransactionForTest.() -> Unit): TransactionForTest {
|
||||||
|
@ -41,7 +41,6 @@ class TransactionForVerification(
|
|||||||
val args: List<VerifiedSigned<Command>>,
|
val args: List<VerifiedSigned<Command>>,
|
||||||
val time: Instant
|
val time: Instant
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun verify(programMap: Map<SecureHash, Contract>) {
|
fun verify(programMap: Map<SecureHash, Contract>) {
|
||||||
// For each input and output state, locate the program to run. Then execute the verification function. If any
|
// For each input and output state, locate the program to run. Then execute the verification function. If any
|
||||||
// throws an exception, the entire transaction is invalid.
|
// throws an exception, the entire transaction is invalid.
|
||||||
|
@ -14,12 +14,18 @@ class ComedyPaperTests {
|
|||||||
)
|
)
|
||||||
val PAPER_2 = PAPER_1.copy(owner = DUMMY_PUBKEY_2)
|
val PAPER_2 = PAPER_1.copy(owner = DUMMY_PUBKEY_2)
|
||||||
|
|
||||||
|
val CASH_1 = Cash.State(InstitutionReference(MINI_CORP, OpaqueBytes.of(1)), 1000.DOLLARS, DUMMY_PUBKEY_1)
|
||||||
|
val CASH_2 = CASH_1.copy(owner = DUMMY_PUBKEY_2)
|
||||||
|
val CASH_3 = CASH_1.copy(owner = DUMMY_PUBKEY_1)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun move() {
|
fun move() {
|
||||||
// One entity sells the paper to another (e.g. the issuer sells it to a first time buyer)
|
|
||||||
transaction {
|
transaction {
|
||||||
|
// One entity sells the paper to another (e.g. the issuer sells it to a first time buyer)
|
||||||
input { PAPER_1 }
|
input { PAPER_1 }
|
||||||
output { PAPER_2 }
|
input { CASH_1 }
|
||||||
|
output("a") { PAPER_2 }
|
||||||
|
output { CASH_2 }
|
||||||
|
|
||||||
this.rejects()
|
this.rejects()
|
||||||
|
|
||||||
@ -28,10 +34,25 @@ class ComedyPaperTests {
|
|||||||
this `fails requirement` "is signed by the owner"
|
this `fails requirement` "is signed by the owner"
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction {
|
arg(DUMMY_PUBKEY_1) { ComedyPaper.Commands.Move() }
|
||||||
arg(DUMMY_PUBKEY_1) { ComedyPaper.Commands.Move() }
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
this.accepts()
|
this.accepts()
|
||||||
}
|
}.chain("a") {
|
||||||
|
arg(DUMMY_PUBKEY_2, MINI_CORP_KEY) { ComedyPaper.Commands.Redeem() }
|
||||||
|
|
||||||
|
// No cash output, can't redeem like that!
|
||||||
|
this.rejects("invalid cash outputs")
|
||||||
|
|
||||||
|
input { CASH_3 }
|
||||||
|
output { CASH_2 }
|
||||||
|
|
||||||
|
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
|
|
||||||
|
// Time passes, but not enough. An attempt to redeem is made.
|
||||||
|
this.rejects("must have matured")
|
||||||
|
|
||||||
|
// Try again at the right time.
|
||||||
|
this.accepts(TEST_TX_TIME + 10.days)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user