From 040e51ec1242981d913547df128cfebb68e31748 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Tue, 21 Jun 2016 18:15:41 +0100 Subject: [PATCH] contracts, core: Expose top-level DSL values/functions to Java by wrapping them in an object core: Add overloads for convenient Java interop contracts, core: Uniform Java interop for tests, use camelCase --- .../r3corda/contracts/testing/TestUtils.kt | 47 +++++--- .../r3corda/contracts/cash/CashTestsJava.java | 58 ++++++++++ .../r3corda/contracts/cash/CashTestsJava.java | 56 --------- .../r3corda/core/contracts/ContractsDSL.kt | 34 ++++-- .../com/r3corda/core/testing/TestUtils.kt | 106 ++++++++++++------ .../contracts/ExperimentalTestUtils.kt | 24 ---- .../testing/ExperimentalTestUtils.kt | 33 ++++++ 7 files changed, 220 insertions(+), 138 deletions(-) create mode 100644 contracts/src/test/java/com/r3corda/contracts/cash/CashTestsJava.java delete mode 100644 contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTestsJava.java delete mode 100644 experimental/src/main/kotlin/com/r3corda/contracts/ExperimentalTestUtils.kt create mode 100644 experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt index e1d01a5079..7c3e9d6650 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/TestUtils.kt @@ -48,25 +48,42 @@ fun generateState() = DummyContract.State(Random().nextInt()) // contract `fails requirement` "some substring of the error message" // } -infix fun Cash.State.`owned by`(owner: PublicKey) = copy(owner = owner) -infix fun Cash.State.`issued by`(party: Party) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit.copy(party = party)))) -infix fun Cash.State.`issued by`(deposit: PartyAndReference) = copy(amount = Amount>(amount.quantity, issuanceDef.copy(issuer = deposit))) -infix fun Cash.State.`with notary`(notary: Party) = TransactionState(this, notary) +// For Java compatibility please define helper methods here and then define the infix notation +object Java { + @JvmStatic fun ownedBy(state: Cash.State, owner: PublicKey) = state.copy(owner = owner) + @JvmStatic fun issuedBy(state: Cash.State, party: Party) = state.copy(amount = Amount>(state.amount.quantity, state.issuanceDef.copy(issuer = state.deposit.copy(party = party)))) + @JvmStatic fun issuedBy(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = Amount>(state.amount.quantity, state.issuanceDef.copy(issuer = deposit))) + @JvmStatic fun withNotary(state: Cash.State, notary: Party) = TransactionState(state, notary) + @JvmStatic fun withDeposit(state: Cash.State, deposit: PartyAndReference) = state.copy(amount = state.amount.copy(token = state.amount.token.copy(issuer = deposit))) -infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = this.copy(owner = owner) -infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary) -infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = this.withOwner(new_owner) + @JvmStatic fun ownedBy(state: CommercialPaper.State, owner: PublicKey) = state.copy(owner = owner) + @JvmStatic fun withNotary(state: CommercialPaper.State, notary: Party) = TransactionState(state, notary) + @JvmStatic fun ownedBy(state: ICommercialPaperState, new_owner: PublicKey) = state.withOwner(new_owner) -infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = - copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) + @JvmStatic fun withNotary(state: ContractState, notary: Party) = TransactionState(state, notary) + + @JvmStatic fun CASH(amount: Amount) = Cash.State( + Amount>(amount.quantity, Issued(DUMMY_CASH_ISSUER, amount.token)), + NullPublicKey) + @JvmStatic fun STATE(amount: Amount>) = Cash.State(amount, NullPublicKey) +} + + +infix fun Cash.State.`owned by`(owner: PublicKey) = Java.ownedBy(this, owner) +infix fun Cash.State.`issued by`(party: Party) = Java.issuedBy(this, party) +infix fun Cash.State.`issued by`(deposit: PartyAndReference) = Java.issuedBy(this, deposit) +infix fun Cash.State.`with notary`(notary: Party) = Java.withNotary(this, notary) +infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = Java.withDeposit(this, deposit) + +infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = Java.ownedBy(this, owner) +infix fun CommercialPaper.State.`with notary`(notary: Party) = Java.withNotary(this, notary) +infix fun ICommercialPaperState.`owned by`(new_owner: PublicKey) = Java.ownedBy(this, new_owner) + +infix fun ContractState.`with notary`(notary: Party) = Java.withNotary(this, notary) val DUMMY_CASH_ISSUER_KEY = generateKeyPair() val DUMMY_CASH_ISSUER = Party("Snake Oil Issuer", DUMMY_CASH_ISSUER_KEY.public).ref(1) /** Allows you to write 100.DOLLARS.CASH */ -val Amount.CASH: Cash.State get() = Cash.State( - Amount>(this.quantity, Issued(DUMMY_CASH_ISSUER, this.token)), - NullPublicKey) +val Amount.CASH: Cash.State get() = Java.CASH(this) +val Amount>.STATE: Cash.State get() = Java.STATE(this) -val Amount>.STATE: Cash.State get() = Cash.State(this, NullPublicKey) - -infix fun ContractState.`with notary`(notary: Party) = TransactionState(this, notary) diff --git a/contracts/src/test/java/com/r3corda/contracts/cash/CashTestsJava.java b/contracts/src/test/java/com/r3corda/contracts/cash/CashTestsJava.java new file mode 100644 index 0000000000..7dfdaaf849 --- /dev/null +++ b/contracts/src/test/java/com/r3corda/contracts/cash/CashTestsJava.java @@ -0,0 +1,58 @@ +package com.r3corda.contracts.cash; + +import com.r3corda.core.contracts.PartyAndReference; +import com.r3corda.core.serialization.OpaqueBytes; +import org.junit.Test; + +import static com.r3corda.core.testing.Java.*; +import static com.r3corda.core.contracts.Java.*; +import static com.r3corda.contracts.testing.Java.*; + +/** + * This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL + */ +public class CashTestsJava { + + private OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1});; + private PartyAndReference defaultIssuer = MEGA_CORP.ref(defaultRef); + private Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), DUMMY_PUBKEY_1); + private Cash.State outState = new Cash.State(inState.getAmount(), DUMMY_PUBKEY_2);; + + @Test + public void trivial() { + + transaction(tx -> { + tx.input(inState); + tx.failsRequirement("the amounts balance"); + + tx.tweak(tw -> { + tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), DUMMY_PUBKEY_2)); + return tw.failsRequirement("the amounts balance"); + }); + + tx.tweak(tw -> { + tw.output(outState); + // No command arguments + return tw.failsRequirement("required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command"); + }); + tx.tweak(tw -> { + tw.output(outState); + tw.arg(DUMMY_PUBKEY_2, new Cash.Commands.Move()); + return tw.failsRequirement("the owning keys are the same as the signing keys"); + }); + tx.tweak(tw -> { + tw.output(outState); + tw.output(issuedBy(outState, MINI_CORP)); + tw.arg(DUMMY_PUBKEY_1, new Cash.Commands.Move()); + return tw.failsRequirement("at least one asset input"); + }); + + // Simple reallocation works. + return tx.tweak(tw -> { + tw.output(outState); + tw.arg(DUMMY_PUBKEY_1, new Cash.Commands.Move()); + return tw.accepts(); + }); + }); + } +} diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTestsJava.java b/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTestsJava.java deleted file mode 100644 index 3b6eae6e33..0000000000 --- a/contracts/src/test/kotlin/com/r3corda/contracts/cash/CashTestsJava.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.r3corda.contracts.cash; - -import com.r3corda.core.contracts.PartyAndReference; -import com.r3corda.core.serialization.OpaqueBytes; -import com.r3corda.core.testing.TransactionTestBase; -import org.junit.Test; - -import static com.r3corda.core.testing.Dummies.*; -import static com.r3corda.contracts.testing.Methods.*; -import static com.r3corda.core.contracts.Currencies.*; -import static com.r3corda.core.contracts.Methods.*; - -public class CashTestsJava extends TransactionTestBase { - - private OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1});; - private PartyAndReference defaultIssuer = MEGA_CORP.ref(defaultRef); - private Cash.State inState = new Cash.State(issued_by(DOLLARS(2000), defaultIssuer), DUMMY_PUBKEY_1); - private Cash.State outState = inState.copy(inState.getAmount(), DUMMY_PUBKEY_2);; - - @Test - public void trivial() { - transaction(begin - .input(inState) - .fails_requirement("the amounts balance") - - .tweak(begin - .output(outState.copy(issued_by(DOLLARS(2000), defaultIssuer), DUMMY_PUBKEY_2)) - .fails_requirement("the amounts balance") - ) - - .tweak(begin - .output(outState) - .fails_requirement("required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command") - ) - - .tweak(begin - .output(outState) - .arg(DUMMY_PUBKEY_2, new Cash.Commands.Move()) - .fails_requirement("the owning keys are the same as the signing keys") - ) - - .tweak(begin - .output(outState) - .output(issued_by(outState, MINI_CORP)) - .arg(DUMMY_PUBKEY_1, new Cash.Commands.Move()) - .fails_requirement("at least one asset input") - ) - - .tweak(begin - .output(outState) - .arg(DUMMY_PUBKEY_1, new Cash.Commands.Move()) - .accepts() - ) - ); - } -} diff --git a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt index 4108c88423..32d370341f 100644 --- a/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/com/r3corda/core/contracts/ContractsDSL.kt @@ -19,18 +19,32 @@ import java.util.* fun currency(code: String) = Currency.getInstance(code) -val USD = currency("USD") -val GBP = currency("GBP") -val CHF = currency("CHF") +// Java interop +object Java { + @JvmField val USD = currency("USD") + @JvmField val GBP = currency("GBP") + @JvmField val CHF = currency("CHF") -val Int.DOLLARS: Amount get() = Amount(this.toLong() * 100, USD) -val Int.POUNDS: Amount get() = Amount(this.toLong() * 100, GBP) -val Int.SWISS_FRANCS: Amount get() = Amount(this.toLong() * 100, CHF) + @JvmStatic fun DOLLARS(amount: Int) = Amount(amount.toLong() * 100, USD) + @JvmStatic fun DOLLARS(amount: Double) = Amount((amount * 100).toLong(), USD) + @JvmStatic fun POUNDS(amount: Int) = Amount(amount.toLong() * 100, GBP) + @JvmStatic fun SWISS_FRANCS(amount: Int) = Amount(amount.toLong() * 100, CHF) -val Double.DOLLARS: Amount get() = Amount((this * 100).toLong(), USD) + @JvmStatic fun issuedBy(currency: Currency, deposit: PartyAndReference) = Issued(deposit, currency) + @JvmStatic fun issuedBy(amount: Amount, deposit: PartyAndReference) = Amount(amount.quantity, issuedBy(amount.token, deposit)) +} -infix fun Currency.`issued by`(deposit: PartyAndReference) : Issued = Issued(deposit, this) -infix fun Amount.`issued by`(deposit: PartyAndReference) : Amount> = Amount(quantity, Issued(deposit, this.token)) +val USD = Java.USD +val GBP = Java.GBP +val CHF = Java.CHF + +val Int.DOLLARS: Amount get() = Java.DOLLARS(this) +val Double.DOLLARS: Amount get() = Java.DOLLARS(this) +val Int.POUNDS: Amount get() = Java.POUNDS(this) +val Int.SWISS_FRANCS: Amount get() = Java.SWISS_FRANCS(this) + +infix fun Currency.`issued by`(deposit: PartyAndReference) = Java.issuedBy(this, deposit) +infix fun Amount.`issued by`(deposit: PartyAndReference) = Java.issuedBy(this, deposit) //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// @@ -111,4 +125,4 @@ inline fun verifyMoveCommand(inputs: List LastLineShouldTestForAcceptOrFailure): LastLineShouldTestForAcceptOrFailure { + return body(TransactionForTest()) + } +} + +val TEST_TX_TIME = Java.TEST_TX_TIME +val MEGA_CORP_KEY = Java.MEGA_CORP_KEY +val MEGA_CORP_PUBKEY = Java.MEGA_CORP_PUBKEY +val MINI_CORP_KEY = Java.MINI_CORP_KEY +val MINI_CORP_PUBKEY = Java.MINI_CORP_PUBKEY +val ORACLE_KEY = Java.ORACLE_KEY +val ORACLE_PUBKEY = Java.ORACLE_PUBKEY +val DUMMY_PUBKEY_1 = Java.DUMMY_PUBKEY_1 +val DUMMY_PUBKEY_2 = Java.DUMMY_PUBKEY_2 +val ALICE_KEY = Java.ALICE_KEY +val ALICE_PUBKEY = Java.ALICE_PUBKEY +val ALICE = Java.ALICE +val BOB_KEY = Java.BOB_KEY +val BOB_PUBKEY = Java.BOB_PUBKEY +val BOB = Java.BOB +val MEGA_CORP = Java.MEGA_CORP +val MINI_CORP = Java.MINI_CORP +val DUMMY_NOTARY_KEY = Java.DUMMY_NOTARY_KEY +val DUMMY_NOTARY = Java.DUMMY_NOTARY +val ALL_TEST_KEYS = Java.ALL_TEST_KEYS +val MOCK_IDENTITY_SERVICE = Java.MOCK_IDENTITY_SERVICE + +fun generateStateRef() = Java.generateStateRef() + +fun transaction(body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure) = Java.transaction(body) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -120,7 +154,10 @@ abstract class AbstractTransactionForTest { protected val signers = LinkedHashSet() protected val type = TransactionType.General() + @JvmOverloads open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, TransactionState(s(), DUMMY_NOTARY)).apply { outStates.add(this) } + @JvmOverloads + open fun output(label: String? = null, s: ContractState) = output(label) { s } protected fun commandsToAuthenticatedObjects(): List> { return commands.map { AuthenticatedObject(it.signers, it.signers.mapNotNull { MOCK_IDENTITY_SERVICE.partyFromKey(it) }, it.value) } @@ -130,10 +167,11 @@ abstract class AbstractTransactionForTest { attachments.add(attachmentID) } - fun arg(vararg key: PublicKey, c: () -> CommandData) { - val keys = listOf(*key) - addCommand(Command(c(), keys)) + fun arg(vararg keys: PublicKey, c: () -> CommandData) { + val keysList = listOf(*keys) + addCommand(Command(c(), keysList)) } + fun arg(key: PublicKey, c: CommandData) = arg(key) { c } fun timestamp(time: Instant) { val data = TimestampCommand(time, 30.seconds) @@ -165,12 +203,15 @@ sealed class LastLineShouldTestForAcceptOrFailure { } // Corresponds to the args to Contract.verify +// Note on defaults: try to avoid Kotlin defaults as they don't work from Java. Instead define overloads open class TransactionForTest : AbstractTransactionForTest() { private val inStates = arrayListOf>() + fun input(s: () -> ContractState) { signers.add(DUMMY_NOTARY.owningKey) inStates.add(TransactionState(s(), DUMMY_NOTARY)) } + fun input(s: ContractState) = input { s } protected fun runCommandsAndVerify(time: Instant) { val cmds = commandsToAuthenticatedObjects() @@ -178,10 +219,13 @@ open class TransactionForTest : AbstractTransactionForTest() { tx.verify() } + @JvmOverloads fun accepts(time: Instant = TEST_TX_TIME): LastLineShouldTestForAcceptOrFailure { runCommandsAndVerify(time) return LastLineShouldTestForAcceptOrFailure.Token } + + @JvmOverloads fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME): LastLineShouldTestForAcceptOrFailure { val r = try { runCommandsAndVerify(time) @@ -202,8 +246,7 @@ open class TransactionForTest : AbstractTransactionForTest() { * Used to confirm that the test, when (implicitly) run against the .verify() method, fails with the text of the message */ infix fun `fails requirement`(msg: String): LastLineShouldTestForAcceptOrFailure = rejects(msg) - - fun fails_requirement(msg: String) = this.`fails requirement`(msg) + fun failsRequirement(msg: String) = this.`fails requirement`(msg) // Use this to create transactions where the output of this transaction is automatically used as an input of // the next. @@ -251,10 +294,6 @@ open class TransactionForTest : AbstractTransactionForTest() { } } -fun transaction(body: TransactionForTest.() -> LastLineShouldTestForAcceptOrFailure): LastLineShouldTestForAcceptOrFailure { - return body(TransactionForTest()) -} - class TransactionGroupDSL(private val stateType: Class) { open inner class WireTransactionDSL : AbstractTransactionForTest() { private val inStates = ArrayList() @@ -335,6 +374,7 @@ class TransactionGroupDSL(private val stateType: Class) { val txns = ArrayList() private val txnToLabelMap = HashMap() + @JvmOverloads fun transaction(label: String? = null, body: WireTransactionDSL.() -> Unit): WireTransaction { val forTest = InternalWireTransactionDSL() forTest.body() diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/ExperimentalTestUtils.kt b/experimental/src/main/kotlin/com/r3corda/contracts/ExperimentalTestUtils.kt deleted file mode 100644 index 247c57a135..0000000000 --- a/experimental/src/main/kotlin/com/r3corda/contracts/ExperimentalTestUtils.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.r3corda.contracts.testing - -import com.r3corda.contracts.Obligation -import com.r3corda.contracts.cash.Cash -import com.r3corda.core.contracts.Amount -import com.r3corda.core.contracts.Issued -import com.r3corda.core.crypto.NullPublicKey -import com.r3corda.core.crypto.Party -import com.r3corda.core.testing.MINI_CORP -import com.r3corda.core.utilities.nonEmptySetOf -import java.security.PublicKey -import java.time.Instant -import java.util.* - -infix fun Obligation.State.`at`(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore)) -infix fun Obligation.State.`between`(parties: Pair) = copy(issuer = parties.first, owner = parties.second) -infix fun Obligation.State.`owned by`(owner: PublicKey) = copy(owner = owner) -infix fun Obligation.State.`issued by`(party: Party) = copy(issuer = party) - -// Allows you to write 100.DOLLARS.OBLIGATION -val Issued.OBLIGATION_DEF: Obligation.StateTemplate get() = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), - nonEmptySetOf(this), Instant.parse("2020-01-01T17:00:00Z")) -val Amount>.OBLIGATION: Obligation.State get() = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, - this.token.OBLIGATION_DEF, this.quantity, NullPublicKey) \ No newline at end of file diff --git a/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt b/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt new file mode 100644 index 0000000000..d88cb52867 --- /dev/null +++ b/experimental/src/main/kotlin/com/r3corda/contracts/testing/ExperimentalTestUtils.kt @@ -0,0 +1,33 @@ +package com.r3corda.contracts.testing + +import com.r3corda.contracts.Obligation +import com.r3corda.contracts.cash.Cash +import com.r3corda.core.contracts.Amount +import com.r3corda.core.contracts.Issued +import com.r3corda.core.crypto.NullPublicKey +import com.r3corda.core.crypto.Party +import com.r3corda.core.testing.MINI_CORP +import com.r3corda.core.utilities.nonEmptySetOf +import java.security.PublicKey +import java.time.Instant +import java.util.* + +object JavaExperimental { + @JvmStatic fun at(state: Obligation.State, dueBefore: Instant) = state.copy(template = state.template.copy(dueBefore = dueBefore)) + @JvmStatic fun between(state: Obligation.State, parties: Pair) = state.copy(issuer = parties.first, owner = parties.second) + @JvmStatic fun ownedBy(state: Obligation.State, owner: PublicKey) = state.copy(owner = owner) + @JvmStatic fun issuedBy(state: Obligation.State, party: Party) = state.copy(issuer = party) + + @JvmStatic fun OBLIGATION_DEF(issued: Issued) + = Obligation.StateTemplate(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(issued), Instant.parse("2020-01-01T17:00:00Z")) + @JvmStatic fun OBLIGATION(amount: Amount>) = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, + OBLIGATION_DEF(amount.token), amount.quantity, NullPublicKey) +} +infix fun Obligation.State.`at`(dueBefore: Instant) = JavaExperimental.at(this, dueBefore) +infix fun Obligation.State.`between`(parties: Pair) = JavaExperimental.between(this, parties) +infix fun Obligation.State.`owned by`(owner: PublicKey) = JavaExperimental.ownedBy(this, owner) +infix fun Obligation.State.`issued by`(party: Party) = JavaExperimental.issuedBy(this, party) + +// Allows you to write 100.DOLLARS.OBLIGATION +val Issued.OBLIGATION_DEF: Obligation.StateTemplate get() = JavaExperimental.OBLIGATION_DEF(this) +val Amount>.OBLIGATION: Obligation.State get() = JavaExperimental.OBLIGATION(this)