Implement and test crafting/generate methods for CommercialPaper.

Rename test keys and use real EC keys instead of dummies.
This commit is contained in:
Mike Hearn 2015-11-27 15:44:43 +01:00
parent d3c15ab7de
commit 1628c1e17a
5 changed files with 118 additions and 36 deletions

View File

@ -94,5 +94,39 @@ class CommercialPaper : Contract {
}
}
}
/**
* Returns a transaction that issues commercial paper, owned by the issuing institution's key. Does not update
* an existing transaction because you aren't able to issue multiple pieces of CP in a single transaction
* at the moment: this restriction is not fundamental and may be lifted later.
*/
fun craftIssue(issuance: InstitutionReference, faceValue: Amount, maturityDate: Instant): PartialTransaction {
val state = State(issuance, issuance.institution.owningKey, faceValue, maturityDate)
return PartialTransaction(state, WireCommand(Commands.Issue, issuance.institution.owningKey))
}
/**
* Updates the given partial transaction with an input/output/command to reassign ownership of the paper.
*/
fun craftMove(tx: PartialTransaction, paper: StateAndRef<State>, newOwner: PublicKey) {
tx.addInputState(paper.ref)
tx.addOutputState(paper.state.copy(owner = newOwner))
tx.addArg(WireCommand(Commands.Move, paper.state.owner))
}
/**
* Intended to be called by the issuer of some commercial paper, when an owner has notified us that they wish
* to redeem the paper. We must therefore send enough money to the key that owns the paper to satisfy the face
* value, and then ensure the paper is removed from the ledger.
*
* @throws InsufficientBalanceException if the wallet doesn't contain enough money to pay the redeemer
*/
@Throws(InsufficientBalanceException::class)
fun craftRedeem(tx: PartialTransaction, paper: StateAndRef<State>, wallet: List<StateAndRef<Cash.State>>) {
// Add the cash movement using the states in our wallet.
Cash().craftSpend(tx, paper.state.faceValue, paper.state.owner, wallet)
tx.addInputState(paper.ref)
tx.addArg(WireCommand(CommercialPaper.Commands.Redeem, paper.state.owner))
}
}

View File

@ -83,10 +83,10 @@ class CashTests {
)
}
tweak {
arg(MINI_CORP_KEY) { Cash.Commands.Issue(0) }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) }
this `fails requirement` "has a nonce"
}
arg(MINI_CORP_KEY) { Cash.Commands.Issue() }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
this.accepts()
}
@ -98,7 +98,7 @@ class CashTests {
assertEquals(MINI_CORP, s.deposit.institution)
assertEquals(DUMMY_PUBKEY_1, s.owner)
assertTrue(ptx.commands()[0].command is Cash.Commands.Issue)
assertEquals(MINI_CORP_KEY, ptx.commands()[0].pubkeys[0])
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].pubkeys[0])
}
@Test
@ -196,13 +196,13 @@ class CashTests {
output { outState.copy(amount = inState.amount - 200.DOLLARS) }
tweak {
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(100.DOLLARS) }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS) }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move }
this `fails requirement` "the amounts balance"
}
tweak {
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
this `fails requirement` "required contracts.Cash.Commands.Move command"
tweak {
@ -223,10 +223,10 @@ class CashTests {
this `fails requirement` "at issuer MegaCorp the amounts balance"
arg(MEGA_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
this `fails requirement` "at issuer MiniCorp the amounts balance"
arg(MINI_CORP_KEY) { Cash.Commands.Exit(200.DOLLARS) }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
this.accepts()
}
}

View File

@ -1,16 +1,16 @@
package contracts
import core.Amount
import core.DOLLARS
import core.days
import core.*
import core.testutils.*
import org.junit.Test
import java.time.Instant
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class CommercialPaperTests {
val PAPER_1 = CommercialPaper.State(
issuance = MEGA_CORP.ref(123),
owner = MEGA_CORP_KEY,
owner = MEGA_CORP_PUBKEY,
faceValue = 1000.DOLLARS,
maturityDate = TEST_TX_TIME + 7.days
)
@ -42,7 +42,7 @@ class CommercialPaperTests {
transactionGroup {
transaction {
output { PAPER_1.copy(faceValue = 0.DOLLARS) }
arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
}
expectFailureOfTx(1, "face value is not zero")
@ -54,7 +54,7 @@ class CommercialPaperTests {
transactionGroup {
transaction {
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
}
expectFailureOfTx(1, "maturity date is not in the past")
@ -70,7 +70,7 @@ class CommercialPaperTests {
transaction {
input("paper")
output { PAPER_1 }
arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
}
expectFailureOfTx(1, "there is no input state")
@ -95,13 +95,13 @@ class CommercialPaperTests {
return transactionGroupFor() {
roots {
transaction(900.DOLLARS.CASH `owned by` ALICE label "alice's $900")
transaction(someProfits.CASH `owned by` MEGA_CORP_KEY label "some profits")
transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits")
}
// Some CP is issued onto the ledger by MegaCorp.
transaction {
output("paper") { PAPER_1 }
arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Issue }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue }
}
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
@ -109,10 +109,10 @@ class CommercialPaperTests {
transaction {
input("paper")
input("alice's $900")
output { 900.DOLLARS.CASH `owned by` MEGA_CORP_KEY }
output { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
output("alice's paper") { "paper".output `owned by` ALICE }
arg(ALICE) { Cash.Commands.Move }
arg(MEGA_CORP_KEY) { CommercialPaper.Commands.Move }
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move }
}
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
@ -122,13 +122,57 @@ class CommercialPaperTests {
input("some profits")
output { aliceGetsBack.CASH `owned by` ALICE }
output { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_KEY }
output { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
if (!destroyPaperAtRedemption)
output { "paper".output }
arg(MEGA_CORP_KEY) { Cash.Commands.Move }
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move }
arg(ALICE) { CommercialPaper.Commands.Redeem }
}
}
}
@Test
fun `issue move and then redeem`() {
val issueTX: LedgerTransaction = run {
val ptx = CommercialPaper().craftIssue(MINI_CORP.ref(123), 10000.DOLLARS, TEST_TX_TIME + 30.days)
ptx.signWith(MINI_CORP_KEY)
val stx = ptx.toSignedTransaction()
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
}
val moveTX: LedgerTransaction = run {
val ptx = PartialTransaction()
CommercialPaper().craftMove(ptx, issueTX.outRef(0), ALICE)
ptx.signWith(MINI_CORP_KEY)
val stx = ptx.toSignedTransaction()
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
}
// Won't be validated.
val someCash = LedgerTransaction(emptyList(), listOf(
9000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY,
4000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY
), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256())
val wallet = listOf<StateAndRef<Cash.State>>(someCash.outRef(0), someCash.outRef(1))
fun makeRedeemTX(time: Instant): LedgerTransaction {
val ptx = PartialTransaction()
CommercialPaper().craftRedeem(ptx, moveTX.outRef(0), wallet)
ptx.signWith(ALICE_KEY)
ptx.signWith(MINI_CORP_KEY)
return ptx.toSignedTransaction().verify().toLedgerTransaction(time, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
}
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days)
val e = assertFailsWith(TransactionVerificationException::class) {
TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(someCash)).verify(TEST_PROGRAM_MAP)
}
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(someCash)).verify(TEST_PROGRAM_MAP)
}
}

View File

@ -8,7 +8,7 @@ import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
class TransactionGroupTests {
val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_KEY)
val A_THOUSAND_POUNDS = Cash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY)
@Test
fun success() {
@ -20,13 +20,13 @@ class TransactionGroupTests {
transaction {
input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE }
arg(MINI_CORP_KEY) { Cash.Commands.Move }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
}
transaction {
input("alice's £1000")
arg(ALICE) { Cash.Commands.Move }
arg(MINI_CORP_KEY) { Cash.Commands.Exit(1000.POUNDS) }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Exit(1000.POUNDS) }
}
verify()
@ -38,7 +38,7 @@ class TransactionGroupTests {
transactionGroup {
val t = transaction {
output("cash") { A_THOUSAND_POUNDS }
arg(MINI_CORP_KEY) { Cash.Commands.Issue() }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
}
val conflict1 = transaction {
@ -46,7 +46,7 @@ class TransactionGroupTests {
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB
output { HALF }
output { HALF }
arg(MINI_CORP_KEY) { Cash.Commands.Move }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
}
verify()
@ -57,7 +57,7 @@ class TransactionGroupTests {
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE
output { HALF }
output { HALF }
arg(MINI_CORP_KEY) { Cash.Commands.Move }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
}
assertNotEquals(conflict1, conflict2)
@ -76,7 +76,7 @@ class TransactionGroupTests {
val tg = transactionGroup {
transaction {
output("cash") { A_THOUSAND_POUNDS }
arg(MINI_CORP_KEY) { Cash.Commands.Issue() }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
}
transaction {
@ -110,7 +110,7 @@ class TransactionGroupTests {
input("£1000")
input("£1000")
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) }
arg(MINI_CORP_KEY) { Cash.Commands.Move }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move }
}
assertFailsWith(TransactionConflictException::class) {

View File

@ -18,18 +18,22 @@ object TestUtils {
}
// A few dummy values for testing.
val MEGA_CORP_KEY = TestUtils.keypair.public
val MINI_CORP_KEY = TestUtils.keypair2.public
val MEGA_CORP_KEY = TestUtils.keypair
val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public
val MINI_CORP_KEY = TestUtils.keypair2
val MINI_CORP_PUBKEY = MINI_CORP_KEY.public
val DUMMY_PUBKEY_1 = DummyPublicKey("x1")
val DUMMY_PUBKEY_2 = DummyPublicKey("x2")
val ALICE = DummyPublicKey("alice")
val BOB = DummyPublicKey("bob")
val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_KEY)
val MINI_CORP = Institution("MiniCorp", MINI_CORP_KEY)
val ALICE_KEY = KeyPairGenerator.getInstance("EC").genKeyPair()
val ALICE = ALICE_KEY.public
val BOB_KEY = KeyPairGenerator.getInstance("EC").genKeyPair()
val BOB = BOB_KEY.public
val MEGA_CORP = Institution("MegaCorp", MEGA_CORP_PUBKEY)
val MINI_CORP = Institution("MiniCorp", MINI_CORP_PUBKEY)
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Institution> = mapOf(
MEGA_CORP_KEY to MEGA_CORP,
MINI_CORP_KEY to MINI_CORP
MEGA_CORP_PUBKEY to MEGA_CORP,
MINI_CORP_PUBKEY to MINI_CORP
)
// A dummy time at which we will be pretending test transactions are created.