mirror of
https://github.com/corda/corda.git
synced 2025-01-18 02:39:51 +00:00
191 lines
7.2 KiB
Kotlin
191 lines
7.2 KiB
Kotlin
package contracts
|
|
|
|
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_PUBKEY,
|
|
faceValue = 1000.DOLLARS,
|
|
maturityDate = TEST_TX_TIME + 7.days
|
|
)
|
|
|
|
@Test
|
|
fun ok() {
|
|
trade().verify()
|
|
}
|
|
|
|
@Test
|
|
fun `not matured at redemption`() {
|
|
trade(redemptionTime = TEST_TX_TIME + 2.days).expectFailureOfTx(3, "must have matured")
|
|
}
|
|
|
|
@Test
|
|
fun `key mismatch at issue`() {
|
|
transactionGroup {
|
|
transaction {
|
|
output { PAPER_1 }
|
|
arg(DUMMY_PUBKEY_1) { CommercialPaper.Commands.Issue() }
|
|
}
|
|
|
|
expectFailureOfTx(1, "signed by the claimed issuer")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `face value is not zero`() {
|
|
transactionGroup {
|
|
transaction {
|
|
output { PAPER_1.copy(faceValue = 0.DOLLARS) }
|
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
|
}
|
|
|
|
expectFailureOfTx(1, "face value is not zero")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `maturity date not in the past`() {
|
|
transactionGroup {
|
|
transaction {
|
|
output { PAPER_1.copy(maturityDate = TEST_TX_TIME - 10.days) }
|
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
|
}
|
|
|
|
expectFailureOfTx(1, "maturity date is not in the past")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `issue cannot replace an existing state`() {
|
|
transactionGroup {
|
|
roots {
|
|
transaction(PAPER_1 label "paper")
|
|
}
|
|
transaction {
|
|
input("paper")
|
|
output { PAPER_1 }
|
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
|
}
|
|
|
|
expectFailureOfTx(1, "there is no input state")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `did not receive enough money at redemption`() {
|
|
trade(aliceGetsBack = 700.DOLLARS).expectFailureOfTx(3, "received amount equals the face value")
|
|
}
|
|
|
|
@Test
|
|
fun `paper must be destroyed by redemption`() {
|
|
trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed")
|
|
}
|
|
|
|
// Generate a trade lifecycle with various parameters.
|
|
private fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
|
aliceGetsBack: Amount = 1000.DOLLARS,
|
|
destroyPaperAtRedemption: Boolean = true): TransactionGroupForTest<CommercialPaper.State> {
|
|
val someProfits = 1200.DOLLARS
|
|
return transactionGroupFor() {
|
|
roots {
|
|
transaction(900.DOLLARS.CASH `owned by` ALICE label "alice's $900")
|
|
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_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,
|
|
// that sounds a bit too good to be true!
|
|
transaction {
|
|
input("paper")
|
|
input("alice's $900")
|
|
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_PUBKEY) { CommercialPaper.Commands.Move() }
|
|
}
|
|
|
|
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
|
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
|
|
transaction(time = redemptionTime) {
|
|
input("alice's paper")
|
|
input("some profits")
|
|
|
|
output { aliceGetsBack.CASH `owned by` ALICE }
|
|
output { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
|
|
if (!destroyPaperAtRedemption)
|
|
output { "paper".output }
|
|
|
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
|
arg(ALICE) { CommercialPaper.Commands.Redeem() }
|
|
}
|
|
}
|
|
}
|
|
|
|
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
|
|
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256())
|
|
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) })
|
|
}
|
|
|
|
@Test
|
|
fun `issue move and then redeem`() {
|
|
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
|
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 (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
|
3000.DOLLARS.CASH `owned by` ALICE,
|
|
3000.DOLLARS.CASH `owned by` ALICE,
|
|
3000.DOLLARS.CASH `owned by` ALICE
|
|
)
|
|
|
|
// Alice pays $9000 to MiniCorp to own some of their debt.
|
|
val moveTX: LedgerTransaction = run {
|
|
val ptx = PartialTransaction()
|
|
Cash().craftSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet)
|
|
CommercialPaper().craftMove(ptx, issueTX.outRef(0), ALICE)
|
|
ptx.signWith(MINI_CORP_KEY)
|
|
ptx.signWith(ALICE_KEY)
|
|
val stx = ptx.toSignedTransaction()
|
|
stx.verify().toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
|
}
|
|
|
|
// Won't be validated.
|
|
val (corpWalletTX, corpWallet) = cashOutputsToWallet(
|
|
9000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY,
|
|
4000.DOLLARS.CASH `owned by` MINI_CORP_PUBKEY
|
|
)
|
|
|
|
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
|
val ptx = PartialTransaction()
|
|
CommercialPaper().craftRedeem(ptx, moveTX.outRef(1), corpWallet)
|
|
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(corpWalletTX, alicesWalletTX)).verify(TEST_PROGRAM_MAP)
|
|
}
|
|
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
|
|
|
|
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(TEST_PROGRAM_MAP)
|
|
}
|
|
} |