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)
|
||||
|
||||
.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!
|
||||
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) {
|
||||
is Commands.Move -> requireThat {
|
||||
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())
|
||||
}
|
||||
|
||||
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?
|
||||
"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 received amount equals the face value" by (received == input.faceValue)
|
||||
"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()
|
||||
} catch (e: NoSuchElementException) {
|
||||
// Better error message.
|
||||
throw IllegalStateException("Required ${T::class.simpleName} command")
|
||||
throw IllegalStateException("Required ${T::class.qualifiedName} command")
|
||||
}
|
||||
|
||||
// endregion
|
||||
@ -51,13 +51,14 @@ val Double.SWISS_FRANCS: Amount get() = Amount((this * 100).toInt(), USD)
|
||||
// region Requirements
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
object Requirements {
|
||||
class Requirements {
|
||||
infix fun String.by(expr: Boolean) {
|
||||
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
|
||||
}
|
||||
}
|
||||
val R = Requirements()
|
||||
inline fun requireThat(body: Requirements.() -> Unit) {
|
||||
Requirements.body()
|
||||
R.body()
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Corresponds to the args to Contract.verify
|
||||
data class TransactionForTest(
|
||||
private val inStates: MutableList<ContractState> = arrayListOf(),
|
||||
private val outStates: MutableList<ContractState> = arrayListOf(),
|
||||
class TransactionForTest() {
|
||||
private val inStates = arrayListOf<ContractState>()
|
||||
|
||||
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()
|
||||
) {
|
||||
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)
|
||||
|
||||
infix fun `fails requirement`(msg: String) {
|
||||
try {
|
||||
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)
|
||||
}
|
||||
constructor(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>) : this() {
|
||||
this.inStates.addAll(inStates)
|
||||
this.outStates.addAll(outStates.map { LabeledOutput(null, it) })
|
||||
this.args.addAll(args)
|
||||
}
|
||||
|
||||
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?? :)
|
||||
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||
|
||||
fun accepts() = run()
|
||||
fun rejects(withMessage: String? = null) {
|
||||
fun accepts(time: Instant = TEST_TX_TIME) = run(time)
|
||||
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
|
||||
val r = try {
|
||||
run()
|
||||
run(time)
|
||||
false
|
||||
} catch (e: Exception) {
|
||||
val m = e.message
|
||||
if (m == null)
|
||||
fail("Threw exception without a message")
|
||||
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
|
||||
}
|
||||
if (!r) throw AssertionError("Expected exception but didn't get one")
|
||||
@ -112,6 +117,21 @@ data class TransactionForTest(
|
||||
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 {
|
||||
return """transaction {
|
||||
inputs: $inStates
|
||||
@ -119,6 +139,15 @@ data class TransactionForTest(
|
||||
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 {
|
||||
|
@ -41,7 +41,6 @@ class TransactionForVerification(
|
||||
val args: List<VerifiedSigned<Command>>,
|
||||
val time: Instant
|
||||
) {
|
||||
|
||||
fun verify(programMap: Map<SecureHash, Contract>) {
|
||||
// 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.
|
||||
|
@ -14,12 +14,18 @@ class ComedyPaperTests {
|
||||
)
|
||||
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
|
||||
fun move() {
|
||||
// One entity sells the paper to another (e.g. the issuer sells it to a first time buyer)
|
||||
transaction {
|
||||
// One entity sells the paper to another (e.g. the issuer sells it to a first time buyer)
|
||||
input { PAPER_1 }
|
||||
output { PAPER_2 }
|
||||
input { CASH_1 }
|
||||
output("a") { PAPER_2 }
|
||||
output { CASH_2 }
|
||||
|
||||
this.rejects()
|
||||
|
||||
@ -28,10 +34,25 @@ class ComedyPaperTests {
|
||||
this `fails requirement` "is signed by the owner"
|
||||
}
|
||||
|
||||
transaction {
|
||||
arg(DUMMY_PUBKEY_1) { ComedyPaper.Commands.Move() }
|
||||
this.accepts()
|
||||
}
|
||||
arg(DUMMY_PUBKEY_1) { ComedyPaper.Commands.Move() }
|
||||
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
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