mirror of
https://github.com/corda/corda.git
synced 2025-02-20 09:26:41 +00:00
CP: check in a "childrens paper" contract that is sort of like commercial paper, but not really. Tests are incomplete.
This commit is contained in:
parent
1298c4b8ed
commit
0131c2cc1d
65
src/contracts/ChildrensPaper.kt
Normal file
65
src/contracts/ChildrensPaper.kt
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package contracts
|
||||||
|
|
||||||
|
import core.*
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Children's paper" is basically like commercial paper, but modelled by a non-expert and without any attention paid
|
||||||
|
* to the issuance aspect. It has a weird name to emphasise that it's not a prototype of real CP, as that's currently
|
||||||
|
* waiting for Jo/Ayoub to deliver full CP use case models. This file may be renamed later if it grows up and
|
||||||
|
* becomes real CP.
|
||||||
|
*
|
||||||
|
* Open issues:
|
||||||
|
* - In this model, you cannot merge or split CP. Can you do this normally? We could model CP as a specialised form
|
||||||
|
* of cash, or reuse some of the cash code?
|
||||||
|
* - Currently cannot trade more than one piece of CP in a single transaction. This is probably going to be a common
|
||||||
|
* issue: need to find a cleaner way to allow this. Does the single-execution-per-transaction model make sense?
|
||||||
|
*/
|
||||||
|
|
||||||
|
val CP_PROGRAM_ID = SecureHash.sha256("childrens-paper")
|
||||||
|
|
||||||
|
data class ChildrensPaperState(
|
||||||
|
val issuance: InstitutionReference,
|
||||||
|
val owner: PublicKey,
|
||||||
|
val faceValue: Amount,
|
||||||
|
val maturityDate: Instant
|
||||||
|
) : ContractState {
|
||||||
|
override val programRef = CP_PROGRAM_ID
|
||||||
|
|
||||||
|
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Generalise the notion of an owned object into a superclass/supercontract. Consider composition vs inheritance.
|
||||||
|
sealed class CPCommands : Command {
|
||||||
|
class MoveCommand : CPCommands()
|
||||||
|
class RedeemCommand : CPCommands()
|
||||||
|
}
|
||||||
|
|
||||||
|
object ChildrensPaper : Contract {
|
||||||
|
override fun verify(inStates: List<ContractState>, outStates: List<ContractState>, args: List<VerifiedSigned<Command>>, time: Instant) {
|
||||||
|
// There are two possible things that can be done with CP. The first is trading it. The second is redeeming it
|
||||||
|
// for cash on or after the maturity date.
|
||||||
|
val command = args.requireSingleCommand<CPCommands>()
|
||||||
|
|
||||||
|
// For now do not allow multiple pieces of CP to trade in a single transaction. Study this more!
|
||||||
|
val input = inStates.filterIsInstance<ChildrensPaperState>().single()
|
||||||
|
val output = outStates.filterIsInstance<ChildrensPaperState>().single()
|
||||||
|
|
||||||
|
when (command.value) {
|
||||||
|
is CPCommands.MoveCommand -> requireThat {
|
||||||
|
"the transaction is signed by the owner of the CP" by (command.signer == input.owner)
|
||||||
|
"the output state is the same as the input state except for owner" by (input.withoutOwner() == output.withoutOwner())
|
||||||
|
}
|
||||||
|
|
||||||
|
is CPCommands.RedeemCommand -> {
|
||||||
|
val received = outStates.sumCashBy(command.signer)
|
||||||
|
requireThat {
|
||||||
|
"the paper must have matured" by (input.maturityDate < time)
|
||||||
|
"the received amount equals the face value" by (received == input.faceValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,9 @@ val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
|
|||||||
MINI_CORP_KEY to MINI_CORP
|
MINI_CORP_KEY to MINI_CORP
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// A dummy time at which we will be pretending test transactions are created.
|
||||||
|
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
// DSL for building pseudo-transactions (not the same as the wire protocol) for testing purposes.
|
||||||
@ -59,21 +62,33 @@ data class TransactionForTest(
|
|||||||
|
|
||||||
infix fun Contract.`fails requirement`(msg: String) {
|
infix fun Contract.`fails requirement`(msg: String) {
|
||||||
try {
|
try {
|
||||||
verify(inStates, outStates, args, Instant.now())
|
verify(inStates, outStates, args, TEST_TX_TIME)
|
||||||
} 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 (!m.contains(msg)) throw AssertionError("Error was actually: $m")
|
if (!m.contains(msg)) throw AssertionError("Error was actually: $m", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// which is uglier?? :)
|
// which is uglier?? :)
|
||||||
fun Contract.fails_requirement(msg: String) = this.`fails requirement`(msg)
|
fun Contract.fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||||
|
|
||||||
fun Contract.accepts() {
|
fun Contract.accepts() = verify(inStates, outStates, args, TEST_TX_TIME)
|
||||||
verify(inStates, outStates, args, Instant.now())
|
fun Contract.rejects(withMessage: String? = null) {
|
||||||
|
val r = try {
|
||||||
|
accepts()
|
||||||
|
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)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
if (!r) throw AssertionError("Expected exception but didn't get one")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow customisation of partial transactions.
|
// Allow customisation of partial transactions.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */
|
/** A simple class that wraps a byte array and makes the equals/hashCode/toString methods work as you actually expect */
|
||||||
@ -19,3 +20,8 @@ open class OpaqueBytes(val bits: ByteArray) {
|
|||||||
|
|
||||||
override fun toString() = "[" + BaseEncoding.base16().encode(bits) + "]"
|
override fun toString() = "[" + BaseEncoding.base16().encode(bits) + "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Int.days: Duration get() = Duration.ofDays(this.toLong())
|
||||||
|
val Int.hours: Duration get() = Duration.ofHours(this.toLong())
|
||||||
|
val Int.minutes: Duration get() = Duration.ofMinutes(this.toLong())
|
||||||
|
val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
|
39
tests/contracts/ChildrensPaperTests.kt
Normal file
39
tests/contracts/ChildrensPaperTests.kt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package contracts
|
||||||
|
|
||||||
|
import core.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
// TODO: Finish this off.
|
||||||
|
|
||||||
|
class ChildrensPaperTests {
|
||||||
|
val contract = ChildrensPaper
|
||||||
|
|
||||||
|
val PAPER_1 = ChildrensPaperState(
|
||||||
|
issuance = InstitutionReference(MEGA_CORP, OpaqueBytes.of(123)),
|
||||||
|
owner = DUMMY_PUBKEY_1,
|
||||||
|
faceValue = 1000.DOLLARS,
|
||||||
|
maturityDate = TEST_TX_TIME + 7.days
|
||||||
|
)
|
||||||
|
val PAPER_2 = PAPER_1.copy(owner = DUMMY_PUBKEY_2)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun move() {
|
||||||
|
// One entity sells the paper to another (e.g. the issuer sells it to a first time buyer)
|
||||||
|
transaction {
|
||||||
|
input { PAPER_1 }
|
||||||
|
output { PAPER_2 }
|
||||||
|
|
||||||
|
contract.rejects()
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
arg(DUMMY_PUBKEY_2) { CPCommands.MoveCommand() }
|
||||||
|
contract `fails requirement` "is signed by the owner"
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
arg(DUMMY_PUBKEY_1) { CPCommands.MoveCommand() }
|
||||||
|
contract.accepts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user