Simplify CashTests and add some comments/convenience APIs to MockServices (#2241)

This commit is contained in:
Mike Hearn 2018-01-25 16:29:26 +01:00 committed by GitHub
parent 242d9cf7ad
commit 17a6f61eba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 291 additions and 298 deletions

View File

@ -497,6 +497,7 @@ object Crypto {
* It returns true if it succeeds, but it always throws an exception if verification fails.
* Strategy on identifying the actual signing scheme is based on the [PublicKey] type, but if the schemeCodeName is known,
* then better use doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray).
*
* @param publicKey the signer's [PublicKey].
* @param signatureData the signatureData on a message.
* @param clearData the clear data/message that was signed (usually the Merkle root).

View File

@ -32,7 +32,7 @@ public class CommercialPaperTest {
private static final TestIdentity BOB = new TestIdentity(BOB_NAME, 80L);
private static final TestIdentity MEGA_CORP = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
private final byte[] defaultRef = {123};
private final MockServices ledgerServices = new MockServices(emptyList(), makeTestIdentityService(MEGA_CORP.getIdentity()));
private final MockServices ledgerServices = new MockServices(MEGA_CORP);
// DOCSTART 1
private ICommercialPaperState getPaper() {

View File

@ -1,27 +1,23 @@
package net.corda.finance.contracts
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.*
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultService
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.days
import net.corda.core.utilities.seconds
import net.corda.finance.DOLLARS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.*
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.finance.contracts.asset.CASH
import net.corda.finance.contracts.asset.Cash
import net.corda.finance.contracts.asset.STATE
import net.corda.testing.core.*
import net.corda.testing.dsl.EnforceVerifyOrFail
import net.corda.testing.dsl.TransactionDSL
import net.corda.testing.dsl.TransactionDSLInterpreter
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
@ -48,15 +44,12 @@ interface ICommercialPaperTestTemplate {
}
private val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
private val MEGA_CORP get() = megaCorp.party
private val MEGA_CORP_IDENTITY get() = megaCorp.identity
private val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
class JavaCommercialPaperTest : ICommercialPaperTestTemplate {
override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State(
MEGA_CORP.ref(123),
MEGA_CORP,
1000.DOLLARS `issued by` MEGA_CORP.ref(123),
megaCorp.ref(123),
megaCorp.party,
1000.DOLLARS `issued by` megaCorp.ref(123),
TEST_TX_TIME + 7.days
)
@ -68,9 +61,9 @@ class JavaCommercialPaperTest : ICommercialPaperTestTemplate {
class KotlinCommercialPaperTest : ICommercialPaperTestTemplate {
override fun getPaper(): ICommercialPaperState = CommercialPaper.State(
issuance = MEGA_CORP.ref(123),
owner = MEGA_CORP,
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
issuance = megaCorp.ref(123),
owner = megaCorp.party,
faceValue = 1000.DOLLARS `issued by` megaCorp.ref(123),
maturityDate = TEST_TX_TIME + 7.days
)
@ -82,9 +75,9 @@ class KotlinCommercialPaperTest : ICommercialPaperTestTemplate {
class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate {
override fun getPaper(): ICommercialPaperState = CommercialPaper.State(
issuance = MEGA_CORP.ref(123),
owner = MEGA_CORP,
faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123),
issuance = megaCorp.ref(123),
owner = megaCorp.party,
faceValue = 1000.DOLLARS `issued by` megaCorp.ref(123),
maturityDate = TEST_TX_TIME + 7.days
)
@ -102,49 +95,36 @@ class CommercialPaperTestsGeneric {
fun data() = listOf(JavaCommercialPaperTest(), KotlinCommercialPaperTest(), KotlinCommercialPaperLegacyTest())
private val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
private val DUMMY_CASH_ISSUER_IDENTITY get() = dummyCashIssuer.identity
private val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1)
private val alice = TestIdentity(ALICE_NAME, 70)
private val BIG_CORP_KEY = generateKeyPair()
private val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
private val alice = TestIdentity(ALICE_NAME, 70)
private val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
private val ALICE get() = alice.party
private val ALICE_KEY get() = alice.keyPair
private val ALICE_PUBKEY get() = alice.publicKey
private val DUMMY_NOTARY get() = dummyNotary.party
private val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
private val MINI_CORP get() = miniCorp.party
private val MINI_CORP_IDENTITY get() = miniCorp.identity
private val MINI_CORP_PUBKEY get() = miniCorp.publicKey
}
@Parameterized.Parameter
lateinit var thisTest: ICommercialPaperTestTemplate
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
val issuer = MEGA_CORP.ref(123)
private val ledgerServices = MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
}, MEGA_CORP.name)
private val megaCorpRef = megaCorp.ref(123)
private val ledgerServices = MockServices(megaCorp, miniCorp)
@Test
fun `trade lifecycle test`() {
val someProfits = 1200.DOLLARS `issued by` issuer
ledgerServices.ledger(DUMMY_NOTARY) {
val someProfits = 1200.DOLLARS `issued by` megaCorpRef
ledgerServices.ledger(dummyNotary.party) {
unverifiedTransaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy ALICE)
output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy MEGA_CORP)
output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy megaCorpRef ownedBy alice.party)
output(Cash.PROGRAM_ID, "some profits", someProfits.STATE ownedBy megaCorp.party)
}
// Some CP is issued onto the ledger by MegaCorp.
transaction("Issuance") {
attachments(CP_PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract(), "paper", thisTest.getPaper())
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party))
timeWindow(TEST_TX_TIME)
this.verifies()
}
@ -155,10 +135,10 @@ class CommercialPaperTestsGeneric {
attachments(Cash.PROGRAM_ID, JavaCommercialPaper.JCP_PROGRAM_ID)
input("paper")
input("alice's $900")
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy MEGA_CORP)
output(thisTest.getContract(), "alice's paper", "paper".output<ICommercialPaperState>().withOwner(ALICE))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(MEGA_CORP_PUBKEY, thisTest.getMoveCommand())
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy megaCorpRef ownedBy megaCorp.party)
output(thisTest.getContract(), "alice's paper", "paper".output<ICommercialPaperState>().withOwner(alice.party))
command(alice.publicKey, Cash.Commands.Move())
command(megaCorp.publicKey, thisTest.getMoveCommand())
this.verifies()
}
@ -170,17 +150,17 @@ class CommercialPaperTestsGeneric {
input("some profits")
fun TransactionDSL<TransactionDSLInterpreter>.outputs(aliceGetsBack: Amount<Issued<Currency>>) {
output(Cash.PROGRAM_ID, "Alice's profit", aliceGetsBack.STATE ownedBy ALICE)
output(Cash.PROGRAM_ID, "Change", (someProfits - aliceGetsBack).STATE ownedBy MEGA_CORP)
output(Cash.PROGRAM_ID, "Alice's profit", aliceGetsBack.STATE ownedBy alice.party)
output(Cash.PROGRAM_ID, "Change", (someProfits - aliceGetsBack).STATE ownedBy megaCorp.party)
}
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
command(ALICE_PUBKEY, thisTest.getRedeemCommand(DUMMY_NOTARY))
command(megaCorp.publicKey, Cash.Commands.Move())
command(alice.publicKey, thisTest.getRedeemCommand(dummyNotary.party))
tweak {
outputs(700.DOLLARS `issued by` issuer)
outputs(700.DOLLARS `issued by` megaCorpRef)
timeWindow(TEST_TX_TIME + 8.days)
this `fails with` "received amount equals the face value"
}
outputs(1000.DOLLARS `issued by` issuer)
outputs(1000.DOLLARS `issued by` megaCorpRef)
tweak {
@ -200,7 +180,7 @@ class CommercialPaperTestsGeneric {
}
private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) = run {
ledgerServices.transaction(DUMMY_NOTARY, script)
ledgerServices.transaction(dummyNotary.party, script)
}
@Test
@ -209,7 +189,7 @@ class CommercialPaperTestsGeneric {
attachment(CP_PROGRAM_ID)
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract(), thisTest.getPaper())
command(MINI_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
command(miniCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party))
timeWindow(TEST_TX_TIME)
this `fails with` "output states are issued by a command signer"
}
@ -220,8 +200,8 @@ class CommercialPaperTestsGeneric {
transaction {
attachment(CP_PROGRAM_ID)
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer))
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
output(thisTest.getContract(), thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` megaCorpRef))
command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party))
timeWindow(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs"
}
@ -233,7 +213,7 @@ class CommercialPaperTestsGeneric {
attachment(CP_PROGRAM_ID)
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
output(thisTest.getContract(), thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days))
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party))
timeWindow(TEST_TX_TIME)
this `fails with` "maturity date is not in the past"
}
@ -246,95 +226,83 @@ class CommercialPaperTestsGeneric {
attachment(JavaCommercialPaper.JCP_PROGRAM_ID)
input(thisTest.getContract(), thisTest.getPaper())
output(thisTest.getContract(), thisTest.getPaper())
command(MEGA_CORP_PUBKEY, thisTest.getIssueCommand(DUMMY_NOTARY))
command(megaCorp.publicKey, thisTest.getIssueCommand(dummyNotary.party))
timeWindow(TEST_TX_TIME)
this `fails with` "output values sum to more than the inputs"
}
}
/**
* Unit test requires two separate Database instances to represent each of the two
* transaction participants (enforces uniqueness of vault content in lieu of partipant identity)
*/
private lateinit var bigCorpServices: MockServices
private lateinit var bigCorpVault: Vault<ContractState>
private lateinit var bigCorpVaultService: VaultService
private lateinit var aliceServices: MockServices
private lateinit var aliceVaultService: VaultService
private lateinit var alicesVault: Vault<ContractState>
private val notaryServices = MockServices(emptyList(), rigorousMock(), MEGA_CORP.name, dummyNotary.keyPair)
private val issuerServices = MockServices(listOf("net.corda.finance.contracts"), rigorousMock(), MEGA_CORP.name, dummyCashIssuer.keyPair)
private lateinit var moveTX: SignedTransaction
@Test
fun `issue move and then redeem`() {
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(
listOf("net.corda.finance.contracts"),
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
TestIdentity(MEGA_CORP.name, ALICE_KEY))
val databaseAlice = aliceDatabaseAndServices.first
aliceServices = aliceDatabaseAndServices.second
aliceVaultService = aliceServices.vaultService
// Set up a test environment with 4 parties:
// 1. The notary
// 2. A dummy cash issuer e.g. central bank
// 3. Alice
// 4. MegaCorp
//
// MegaCorp will issue some commercial paper and Alice will buy it, using cash issued to her in the name
// of the dummy cash issuer.
databaseAlice.transaction {
alicesVault = VaultFiller(aliceServices, dummyNotary, rngFactory = ::Random).fillWithSomeTestCash(9000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER)
aliceVaultService = aliceServices.vaultService
val allIdentities = arrayOf(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity)
val notaryServices = MockServices(dummyNotary)
val issuerServices = MockServices(dummyCashIssuer, dummyNotary)
val (aliceDatabase, aliceServices) = makeTestDatabaseAndMockServices(
listOf("net.corda.finance.contracts"),
makeTestIdentityService(*allIdentities),
alice
)
val aliceCash: Vault<Cash.State> = aliceDatabase.transaction {
VaultFiller(aliceServices, dummyNotary).fillWithSomeTestCash(9000.DOLLARS, issuerServices, 1, dummyCashIssuer.ref(1))
}
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(
listOf("net.corda.finance.contracts"),
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
TestIdentity(MEGA_CORP.name, BIG_CORP_KEY))
val databaseBigCorp = bigCorpDatabaseAndServices.first
bigCorpServices = bigCorpDatabaseAndServices.second
bigCorpVaultService = bigCorpServices.vaultService
databaseBigCorp.transaction {
bigCorpVault = VaultFiller(bigCorpServices, dummyNotary, rngFactory = ::Random).fillWithSomeTestCash(13000.DOLLARS, issuerServices, 1, DUMMY_CASH_ISSUER)
bigCorpVaultService = bigCorpServices.vaultService
val (megaCorpDatabase, megaCorpServices) = makeTestDatabaseAndMockServices(
listOf("net.corda.finance.contracts"),
makeTestIdentityService(*allIdentities),
megaCorp
)
val bigCorpCash: Vault<Cash.State> = megaCorpDatabase.transaction {
VaultFiller(megaCorpServices, dummyNotary).fillWithSomeTestCash(13000.DOLLARS, issuerServices, 1, dummyCashIssuer.ref(1))
}
// Propagate the cash transactions to each side.
aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
aliceServices.recordTransactions(bigCorpCash.states.map { megaCorpServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
megaCorpServices.recordTransactions(aliceCash.states.map { aliceServices.validatedTransactions.getTransaction(it.ref.txhash)!! })
// BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER
val issuance = bigCorpServices.myInfo.chooseIdentity().ref(1)
val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY)
// MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
val faceValue = 10000.DOLLARS `issued by` dummyCashIssuer.ref(1)
val issuance = megaCorpServices.myInfo.chooseIdentity().ref(1)
val issueBuilder = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, dummyNotary.party)
issueBuilder.setTimeWindow(TEST_TX_TIME, 30.seconds)
val issuePtx = bigCorpServices.signInitialTransaction(issueBuilder)
val issuePtx = megaCorpServices.signInitialTransaction(issueBuilder)
val issueTx = notaryServices.addSignature(issuePtx)
databaseAlice.transaction {
val moveTX = aliceDatabase.transaction {
// Alice pays $9000 to BigCorp to own some of their debt.
moveTX = run {
val builder = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(BIG_CORP_KEY.public))
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(ALICE_KEY.public))
val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = bigCorpServices.addSignature(ptx)
val stx = notaryServices.addSignature(ptx2)
stx
}
val builder = TransactionBuilder(dummyNotary.party)
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, alice.identity, AnonymousParty(megaCorp.publicKey))
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(alice.keyPair.public))
val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = megaCorpServices.addSignature(ptx)
val stx = notaryServices.addSignature(ptx2)
stx
}
databaseBigCorp.transaction {
megaCorpDatabase.transaction {
// Verify the txns are valid and insert into both sides.
listOf(issueTx, moveTX).forEach {
it.toLedgerTransaction(aliceServices).verify()
aliceServices.recordTransactions(it)
bigCorpServices.recordTransactions(it)
for (tx in listOf(issueTx, moveTX)) {
tx.toLedgerTransaction(aliceServices).verify()
aliceServices.recordTransactions(tx)
megaCorpServices.recordTransactions(tx)
}
}
databaseBigCorp.transaction {
megaCorpDatabase.transaction {
fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> {
val builder = TransactionBuilder(DUMMY_NOTARY)
val builder = TransactionBuilder(dummyNotary.party)
builder.setTimeWindow(time, 30.seconds)
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices, bigCorpServices.myInfo.chooseIdentityAndCert())
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), megaCorpServices, megaCorpServices.myInfo.chooseIdentityAndCert())
val ptx = aliceServices.signInitialTransaction(builder)
val ptx2 = bigCorpServices.addSignature(ptx)
val ptx2 = megaCorpServices.addSignature(ptx)
val stx = notaryServices.addSignature(ptx2)
return Pair(stx, builder.lockId)
}
@ -347,7 +315,7 @@ class CommercialPaperTestsGeneric {
}
// manually release locks held by this failing transaction
aliceServices.vaultService.softLockRelease(tooEarlyRedemptionLockId)
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
assertTrue("paper must have matured" in e.cause!!.message!!)
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days).first
validRedemption.toLedgerTransaction(aliceServices).verify()

View File

@ -1,9 +1,11 @@
package net.corda.finance.contracts.asset
import com.nhaarman.mockito_kotlin.*
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
@ -15,16 +17,14 @@ import net.corda.finance.utils.sumCash
import net.corda.finance.utils.sumCashBy
import net.corda.finance.utils.sumCashOrNull
import net.corda.finance.utils.sumCashOrZero
import net.corda.node.services.api.IdentityServiceInternal
import net.corda.node.services.vault.NodeVaultService
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.testing.contracts.DummyState
import net.corda.testing.core.*
import net.corda.testing.internal.LogHelper
import net.corda.testing.dsl.EnforceVerifyOrFail
import net.corda.testing.dsl.TransactionDSL
import net.corda.testing.dsl.TransactionDSLInterpreter
import net.corda.testing.internal.rigorousMock
import net.corda.testing.internal.LogHelper
import net.corda.testing.internal.vault.VaultFiller
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDatabaseAndMockServices
@ -41,41 +41,25 @@ import kotlin.test.*
class CashTests {
private companion object {
val alice = TestIdentity(ALICE_NAME, 70)
val BOB_PUBKEY = TestIdentity(BOB_NAME, 80).publicKey
val bob = TestIdentity(BOB_NAME, 80)
val charlie = TestIdentity(CHARLIE_NAME, 90)
val DUMMY_CASH_ISSUER_IDENTITY = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10).identity
val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB"))
val ALICE get() = alice.party
val ALICE_PUBKEY get() = alice.publicKey
val CHARLIE get() = charlie.party
val CHARLIE_IDENTITY get() = charlie.identity
val DUMMY_NOTARY get() = dummyNotary.party
val DUMMY_NOTARY_IDENTITY get() = dummyNotary.identity
val DUMMY_NOTARY_KEY get() = dummyNotary.keyPair
val MEGA_CORP get() = megaCorp.party
val MEGA_CORP_IDENTITY get() = megaCorp.identity
val MEGA_CORP_KEY get() = megaCorp.keyPair
val MEGA_CORP_PUBKEY get() = megaCorp.publicKey
val MINI_CORP get() = miniCorp.party
val MINI_CORP_IDENTITY get() = miniCorp.identity
val MINI_CORP_KEY get() = miniCorp.keyPair
val MINI_CORP_PUBKEY get() = miniCorp.publicKey
}
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private val defaultRef = OpaqueBytes.of(1)
private val defaultIssuer = MEGA_CORP.ref(defaultRef)
private val defaultIssuer = megaCorp.ref(1)
private val inState = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = AnonymousParty(ALICE_PUBKEY)
owner = AnonymousParty(alice.publicKey)
)
// Input state held by the issuer
private val issuerInState = inState.copy(owner = defaultIssuer.party)
private val outState = issuerInState.copy(owner = AnonymousParty(BOB_PUBKEY))
private val outState = issuerInState.copy(owner = AnonymousParty(bob.publicKey))
private fun Cash.State.editDepositRef(ref: Byte) = copy(
amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref))))
@ -90,48 +74,42 @@ class CashTests {
private lateinit var ourIdentity: AbstractParty
private lateinit var miniCorpAnonymised: AnonymousParty
private val CHARLIE_ANONYMISED = CHARLIE_IDENTITY.party.anonymise()
private lateinit var WALLET: List<StateAndRef<Cash.State>>
private lateinit var cashStates: List<StateAndRef<Cash.State>>
// TODO: Optimise this so that we don't throw away and rebuild state that can be shared across tests.
@Before
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
megaCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), MEGA_CORP.name, MEGA_CORP_KEY)
miniCorpServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
doNothing().whenever(it).justVerifyAndRegisterIdentity(argThat { name == MINI_CORP.name })
}, MINI_CORP.name, MINI_CORP_KEY)
val notaryServices = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock(), DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
megaCorpServices = MockServices(megaCorp)
miniCorpServices = MockServices(miniCorp)
val databaseAndServices = makeTestDatabaseAndMockServices(
listOf("net.corda.finance.contracts.asset"),
makeTestIdentityService(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY),
TestIdentity(CordaX500Name("Me", "London", "GB")))
makeTestIdentityService(megaCorp.identity, miniCorp.identity, dummyCashIssuer.identity, dummyNotary.identity),
TestIdentity(CordaX500Name("Me", "London", "GB"))
)
database = databaseAndServices.first
ourServices = databaseAndServices.second
// Set up and register identities
ourIdentity = ourServices.myInfo.singleIdentity()
miniCorpAnonymised = miniCorpServices.myInfo.singleIdentityAndCert().party.anonymise()
(miniCorpServices.myInfo.legalIdentitiesAndCerts + megaCorpServices.myInfo.legalIdentitiesAndCerts + notaryServices.myInfo.legalIdentitiesAndCerts).forEach { identity ->
ourServices.identityService.verifyAndRegisterIdentity(identity) // TODO: Configure a mock identity service instead.
}
// Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved.
database.transaction {
val vaultFiller = VaultFiller(ourServices, dummyNotary, rngFactory = ::Random)
vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, MEGA_CORP.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, MINI_CORP.ref(1), ourIdentity)
val vaultFiller = VaultFiller(ourServices, dummyNotary)
vaultFiller.fillWithSomeTestCash(100.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(400.DOLLARS, megaCorpServices, 1, megaCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.DOLLARS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)
vaultFiller.fillWithSomeTestCash(80.SWISS_FRANCS, miniCorpServices, 1, miniCorp.ref(1), ourIdentity)
}
database.transaction {
vaultStatesUnconsumed = ourServices.vaultService.queryBy<Cash.State>().states
}
WALLET = listOf(
makeCash(100.DOLLARS, MEGA_CORP),
makeCash(400.DOLLARS, MEGA_CORP),
makeCash(80.DOLLARS, MINI_CORP),
makeCash(80.SWISS_FRANCS, MINI_CORP, 2)
cashStates = listOf(
makeCash(100.DOLLARS, megaCorp.party),
makeCash(400.DOLLARS, megaCorp.party),
makeCash(80.DOLLARS, miniCorp.party),
makeCash(80.SWISS_FRANCS, miniCorp.party, 2)
)
}
@ -140,13 +118,8 @@ class CashTests {
database.close()
}
private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) = run {
MockServices(emptyList(), rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
doReturn(MINI_CORP).whenever(it).partyFromKey(MINI_CORP_PUBKEY)
doReturn(null).whenever(it).partyFromKey(ALICE_PUBKEY)
doReturn(null).whenever(it).partyFromKey(BOB_PUBKEY)
}, MEGA_CORP.name).transaction(DUMMY_NOTARY, script)
private fun transaction(script: TransactionDSL<TransactionDSLInterpreter>.() -> EnforceVerifyOrFail) {
MockServices(megaCorp).transaction(dummyNotary.party, script)
}
@Test
@ -156,30 +129,30 @@ class CashTests {
input(Cash.PROGRAM_ID, inState)
tweak {
output(Cash.PROGRAM_ID, outState.copy(amount = 2000.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
tweak {
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, DummyCommandData)
command(alice.publicKey, DummyCommandData)
// Invalid command
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
}
tweak {
output(Cash.PROGRAM_ID, outState)
command(BOB_PUBKEY, Cash.Commands.Move())
command(bob.publicKey, Cash.Commands.Move())
this `fails with` "the owning keys are a subset of the signing keys"
}
tweak {
output(Cash.PROGRAM_ID, outState)
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
command(ALICE_PUBKEY, Cash.Commands.Move())
output(Cash.PROGRAM_ID, outState issuedBy miniCorp.party)
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "at least one cash input"
}
// Simple reallocation works.
tweak {
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this.verifies()
}
}
@ -192,7 +165,7 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, DummyState())
output(Cash.PROGRAM_ID, outState)
command(MINI_CORP_PUBKEY, Cash.Commands.Move())
command(miniCorp.publicKey, Cash.Commands.Move())
this `fails with` "there is at least one cash input for this group"
}
}
@ -204,16 +177,16 @@ class CashTests {
transaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, Cash.Commands.Issue())
command(alice.publicKey, Cash.Commands.Issue())
this `fails with` "output states are issued by a command signer"
}
transaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID,
Cash.State(
amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34),
owner = AnonymousParty(ALICE_PUBKEY)))
command(MINI_CORP_PUBKEY, Cash.Commands.Issue())
amount = 1000.DOLLARS `issued by` miniCorp.ref(12, 34),
owner = AnonymousParty(alice.publicKey)))
command(miniCorp.publicKey, Cash.Commands.Issue())
this.verifies()
}
}
@ -222,23 +195,23 @@ class CashTests {
fun generateIssueRaw() {
// Test generation works.
val tx: WireTransaction = TransactionBuilder(notary = null).apply {
Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
Cash().generateIssue(this, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = AnonymousParty(alice.publicKey), notary = dummyNotary.party)
}.toWireTransaction(miniCorpServices)
assertTrue(tx.inputs.isEmpty())
val s = tx.outputsOfType<Cash.State>().single()
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
assertEquals(MINI_CORP as AbstractParty, s.amount.token.issuer.party)
assertEquals(AnonymousParty(ALICE_PUBKEY), s.owner)
assertEquals(100.DOLLARS `issued by` miniCorp.ref(12, 34), s.amount)
assertEquals(miniCorp.party as AbstractParty, s.amount.token.issuer.party)
assertEquals(AnonymousParty(alice.publicKey), s.owner)
assertTrue(tx.commands[0].value is Cash.Commands.Issue)
assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
assertEquals(miniCorp.publicKey, tx.commands[0].signers[0])
}
@Test
fun generateIssueFromAmount() {
// Test issuance from an issued amount
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
val amount = 100.DOLLARS `issued by` miniCorp.ref(12, 34)
val tx: WireTransaction = TransactionBuilder(notary = null).apply {
Cash().generateIssue(this, amount, owner = AnonymousParty(ALICE_PUBKEY), notary = DUMMY_NOTARY)
Cash().generateIssue(this, amount, owner = AnonymousParty(alice.publicKey), notary = dummyNotary.party)
}.toWireTransaction(miniCorpServices)
assertTrue(tx.inputs.isEmpty())
assertEquals(tx.outputs[0], tx.outputs[0])
@ -253,13 +226,13 @@ class CashTests {
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
// Move fails: not allowed to summon money.
tweak {
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Issue works.
tweak {
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
command(megaCorp.publicKey, Cash.Commands.Issue())
this.verifies()
}
}
@ -269,7 +242,7 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount.splitEvenly(2).first()))
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
command(megaCorp.publicKey, Cash.Commands.Issue())
this `fails with` "output values sum to more than the inputs"
}
@ -278,7 +251,7 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState)
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
command(megaCorp.publicKey, Cash.Commands.Issue())
this `fails with` "output values sum to more than the inputs"
}
@ -287,9 +260,9 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState.copy(amount = inState.amount * 2))
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
command(megaCorp.publicKey, Cash.Commands.Issue())
tweak {
command(MEGA_CORP_PUBKEY, Cash.Commands.Issue())
command(megaCorp.publicKey, Cash.Commands.Issue())
this `fails with` "there is only a single issue command"
}
this.verifies()
@ -303,15 +276,15 @@ class CashTests {
@Test(expected = IllegalStateException::class)
fun `reject issuance with inputs`() {
// Issue some cash
var ptx = TransactionBuilder(DUMMY_NOTARY)
var ptx = TransactionBuilder(dummyNotary.party)
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
Cash().generateIssue(ptx, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = miniCorp.party, notary = dummyNotary.party)
val tx = miniCorpServices.signInitialTransaction(ptx)
// Include the previously issued cash in a new issuance command
ptx = TransactionBuilder(DUMMY_NOTARY)
ptx = TransactionBuilder(dummyNotary.party)
ptx.addInputState(tx.tx.outRef<Cash.State>(0))
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY)
Cash().generateIssue(ptx, 100.DOLLARS `issued by` miniCorp.ref(12, 34), owner = miniCorp.party, notary = dummyNotary.party)
}
@Test
@ -319,7 +292,7 @@ class CashTests {
// Splitting value works.
transaction {
attachment(Cash.PROGRAM_ID)
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
tweak {
input(Cash.PROGRAM_ID, inState)
val splits4 = inState.amount.splitEvenly(4)
@ -350,7 +323,7 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "zero sized inputs"
}
transaction {
@ -358,7 +331,7 @@ class CashTests {
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, inState.copy(amount = 0.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "zero sized outputs"
}
}
@ -369,8 +342,8 @@ class CashTests {
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, outState issuedBy MINI_CORP)
command(ALICE_PUBKEY, Cash.Commands.Move())
output(Cash.PROGRAM_ID, outState issuedBy miniCorp.party)
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't change deposit reference when splitting.
@ -379,7 +352,7 @@ class CashTests {
val splits2 = inState.amount.splitEvenly(2)
input(Cash.PROGRAM_ID, inState)
for (i in 0..1) output(Cash.PROGRAM_ID, outState.copy(amount = splits2[i]).editDepositRef(i.toByte()))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't mix currencies.
@ -388,7 +361,7 @@ class CashTests {
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, outState.copy(amount = 800.DOLLARS `issued by` defaultIssuer))
output(Cash.PROGRAM_ID, outState.copy(amount = 200.POUNDS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
transaction {
@ -397,18 +370,18 @@ class CashTests {
input(Cash.PROGRAM_ID,
inState.copy(
amount = 150.POUNDS `issued by` defaultIssuer,
owner = AnonymousParty(BOB_PUBKEY)))
owner = AnonymousParty(bob.publicKey)))
output(Cash.PROGRAM_ID, outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't have superfluous input states from different issuers.
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
input(Cash.PROGRAM_ID, inState issuedBy miniCorp.party)
output(Cash.PROGRAM_ID, outState)
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
// Can't combine two different deposits at the same issuer.
@ -417,7 +390,7 @@ class CashTests {
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState.editDepositRef(3))
output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount * 2).editDepositRef(3))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "for reference [01]"
}
}
@ -430,17 +403,17 @@ class CashTests {
input(Cash.PROGRAM_ID, issuerInState)
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
tweak {
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
command(megaCorp.publicKey, Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer))
command(megaCorp.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
tweak {
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command"
tweak {
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
command(megaCorp.publicKey, Cash.Commands.Move())
this.verifies()
}
}
@ -453,14 +426,14 @@ class CashTests {
transaction {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, issuerInState)
input(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP) issuedBy MINI_CORP)
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy MINI_CORP)
output(Cash.PROGRAM_ID, issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
command(listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Cash.Commands.Move())
input(Cash.PROGRAM_ID, issuerInState.copy(owner = miniCorp.party) issuedBy miniCorp.party)
output(Cash.PROGRAM_ID, issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) issuedBy miniCorp.party)
output(Cash.PROGRAM_ID, issuerInState.copy(owner = miniCorp.party, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)))
command(listOf(megaCorp.publicKey, miniCorp.publicKey), Cash.Commands.Move())
this `fails with` "the amounts balance"
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
this `fails with` "the amounts balance"
command(MINI_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` MINI_CORP.ref(defaultRef)))
command(miniCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` miniCorp.ref(1)))
this.verifies()
}
}
@ -472,8 +445,8 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
input(Cash.PROGRAM_ID, inState)
output(Cash.PROGRAM_ID, outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)))
command(MEGA_CORP_PUBKEY, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
command(ALICE_PUBKEY, Cash.Commands.Move())
command(megaCorp.publicKey, Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer))
command(alice.publicKey, Cash.Commands.Move())
this `fails with` "the amounts balance"
}
}
@ -484,23 +457,23 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
// Gather 2000 dollars from two different issuers.
input(Cash.PROGRAM_ID, inState)
input(Cash.PROGRAM_ID, inState issuedBy MINI_CORP)
command(ALICE_PUBKEY, Cash.Commands.Move())
input(Cash.PROGRAM_ID, inState issuedBy miniCorp.party)
command(alice.publicKey, Cash.Commands.Move())
// Can't merge them together.
tweak {
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY), amount = 2000.DOLLARS `issued by` defaultIssuer))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey), amount = 2000.DOLLARS `issued by` defaultIssuer))
this `fails with` "the amounts balance"
}
// Missing MiniCorp deposit
tweak {
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)))
this `fails with` "the amounts balance"
}
// This works.
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(BOB_PUBKEY)) issuedBy MINI_CORP)
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)))
output(Cash.PROGRAM_ID, inState.copy(owner = AnonymousParty(bob.publicKey)) issuedBy miniCorp.party)
this.verifies()
}
}
@ -510,12 +483,12 @@ class CashTests {
// Check we can do an atomic currency trade tx.
transaction {
attachment(Cash.PROGRAM_ID)
val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(BOB_PUBKEY))
input(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(ALICE_PUBKEY))
val pounds = Cash.State(658.POUNDS `issued by` miniCorp.ref(3, 4, 5), AnonymousParty(bob.publicKey))
input(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(alice.publicKey))
input(Cash.PROGRAM_ID, pounds)
output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(BOB_PUBKEY))
output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(ALICE_PUBKEY))
command(listOf(ALICE_PUBKEY, BOB_PUBKEY), Cash.Commands.Move())
output(Cash.PROGRAM_ID, inState ownedBy AnonymousParty(bob.publicKey))
output(Cash.PROGRAM_ID, pounds ownedBy AnonymousParty(alice.publicKey))
command(listOf(alice.publicKey, bob.publicKey), Cash.Commands.Move())
this.verifies()
}
}
@ -526,7 +499,7 @@ class CashTests {
private fun makeCash(amount: Amount<Currency>, issuer: AbstractParty, depositRef: Byte = 1) =
StateAndRef(
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, DUMMY_NOTARY),
TransactionState(Cash.State(amount `issued by` issuer.ref(depositRef), ourIdentity), Cash.PROGRAM_ID, dummyNotary.party),
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
)
@ -534,15 +507,15 @@ class CashTests {
* Generate an exit transaction, removing some amount of cash from the ledger.
*/
private fun makeExit(serviceHub: ServiceHub, amount: Amount<Currency>, issuer: Party, depositRef: Byte = 1): WireTransaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(MINI_CORP_IDENTITY, false).party
Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), WALLET, payChangeTo)
val tx = TransactionBuilder(dummyNotary.party)
val payChangeTo = serviceHub.keyManagementService.freshKeyAndCert(miniCorp.identity, false).party
Cash().generateExit(tx, Amount(amount.quantity, Issued(issuer.ref(depositRef), amount.token)), cashStates, payChangeTo)
return tx.toWireTransaction(serviceHub)
}
private fun makeSpend(services: ServiceHub, amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
val ourIdentity = services.myInfo.singleIdentityAndCert()
val tx = TransactionBuilder(DUMMY_NOTARY)
val tx = TransactionBuilder(dummyNotary.party)
database.transaction {
Cash.generateSpend(services, tx, amount, ourIdentity, dest)
}
@ -554,12 +527,12 @@ class CashTests {
*/
@Test
fun generateSimpleExit() {
val wtx = makeExit(miniCorpServices, 100.DOLLARS, MEGA_CORP, 1)
assertEquals(WALLET[0].ref, wtx.inputs[0])
val wtx = makeExit(miniCorpServices, 100.DOLLARS, megaCorp.party, 1)
assertEquals(cashStates[0].ref, wtx.inputs[0])
assertEquals(0, wtx.outputs.size)
val expectedMove = Cash.Commands.Move()
val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(megaCorp.ref(1), USD)))
assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value })
}
@ -569,15 +542,15 @@ class CashTests {
*/
@Test
fun generatePartialExit() {
val wtx = makeExit(miniCorpServices, 50.DOLLARS, MEGA_CORP, 1)
val wtx = makeExit(miniCorpServices, 50.DOLLARS, megaCorp.party, 1)
val actualInput = wtx.inputs.single()
// Filter the available inputs and confirm exactly one has been used
val expectedInputs = WALLET.filter { it.ref == actualInput }
val expectedInputs = cashStates.filter { it.ref == actualInput }
assertEquals(1, expectedInputs.size)
val inputState = expectedInputs.single()
val actualChange = wtx.outputs.single().data as Cash.State
val expectedChangeAmount = inputState.state.data.amount.quantity - 50.DOLLARS.quantity
val expectedChange = WALLET[0].state.data.copy(amount = WALLET[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner)
val expectedChange = cashStates[0].state.data.copy(amount = cashStates[0].state.data.amount.copy(quantity = expectedChangeAmount), owner = actualChange.owner)
assertEquals(expectedChange, wtx.getOutput(0))
}
@ -586,7 +559,7 @@ class CashTests {
*/
@Test
fun generateAbsentExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 1) }
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, megaCorp.party, 1) }
}
/**
@ -594,7 +567,7 @@ class CashTests {
*/
@Test
fun generateInvalidReferenceExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, MEGA_CORP, 2) }
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, megaCorp.party, 2) }
}
/**
@ -602,7 +575,7 @@ class CashTests {
*/
@Test
fun generateInsufficientExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, MEGA_CORP, 1) }
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 1000.DOLLARS, megaCorp.party, 1) }
}
/**
@ -610,7 +583,7 @@ class CashTests {
*/
@Test
fun generateOwnerWithNoStatesExit() {
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, CHARLIE, 1) }
assertFailsWith<InsufficientBalanceException> { makeExit(miniCorpServices, 100.POUNDS, charlie.party, 1) }
}
/**
@ -619,8 +592,8 @@ class CashTests {
@Test
fun generateExitWithEmptyVault() {
assertFailsWith<IllegalArgumentException> {
val tx = TransactionBuilder(DUMMY_NOTARY)
Cash().generateExit(tx, Amount(100, Issued(CHARLIE.ref(1), GBP)), emptyList(), ourIdentity)
val tx = TransactionBuilder(dummyNotary.party)
Cash().generateExit(tx, Amount(100, Issued(charlie.ref(1), GBP)), emptyList(), ourIdentity)
}
}
@ -641,9 +614,8 @@ class CashTests {
@Test
fun generateSimpleSpendWithParties() {
database.transaction {
val tx = TransactionBuilder(DUMMY_NOTARY)
Cash.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), ALICE, setOf(MINI_CORP))
val tx = TransactionBuilder(dummyNotary.party)
Cash.generateSpend(ourServices, tx, 80.DOLLARS, ourServices.myInfo.singleIdentityAndCert(), alice.party, setOf(miniCorp.party))
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
}
@ -731,9 +703,9 @@ class CashTests {
*/
@Test
fun aggregation() {
val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP)
val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP)
val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP)
val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` megaCorp.ref(2), megaCorp.party)
val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` megaCorp.ref(2), miniCorp.party)
val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` miniCorp.ref(3), megaCorp.party)
// Obviously it must be possible to aggregate states with themselves
assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token)
@ -747,7 +719,7 @@ class CashTests {
// States cannot be aggregated if the currency differs
assertNotEquals(oneThousandDollarsFromMini.amount.token,
Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token)
Cash.State(1000.POUNDS `issued by` miniCorp.ref(3), megaCorp.party).amount.token)
// States cannot be aggregated if the reference differs
assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega withDeposit defaultIssuer).amount.token)
@ -757,20 +729,20 @@ class CashTests {
@Test
fun `summing by owner`() {
val states = listOf(
Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP),
Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
Cash.State(1000.DOLLARS `issued by` defaultIssuer, miniCorp.party),
Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party)
)
assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP))
assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(megaCorp.party))
}
@Test(expected = UnsupportedOperationException::class)
fun `summing by owner throws`() {
val states = listOf(
Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party)
)
states.sumCashBy(MINI_CORP)
states.sumCashBy(miniCorp.party)
}
@Test
@ -789,9 +761,9 @@ class CashTests {
@Test
fun `summing a single currency`() {
val states = listOf(
Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP)
Cash.State(1000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
Cash.State(2000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
Cash.State(4000.DOLLARS `issued by` defaultIssuer, megaCorp.party)
)
// Test that summing everything produces the total number of dollars
val expected = 7000.DOLLARS `issued by` defaultIssuer
@ -802,8 +774,8 @@ class CashTests {
@Test(expected = IllegalArgumentException::class)
fun `summing multiple currencies`() {
val states = listOf(
Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP),
Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP)
Cash.State(1000.DOLLARS `issued by` defaultIssuer, megaCorp.party),
Cash.State(4000.POUNDS `issued by` defaultIssuer, megaCorp.party)
)
// Test that summing everything fails because we're mixing units
states.sumCash()
@ -812,23 +784,20 @@ class CashTests {
// Double spend.
@Test
fun chainCashDoubleSpendFailsWith() {
val mockService = MockServices(listOf("net.corda.finance.contracts.asset"), rigorousMock<IdentityServiceInternal>().also {
doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY)
}, MEGA_CORP.name, MEGA_CORP_KEY)
mockService.ledger(DUMMY_NOTARY) {
MockServices(megaCorp).ledger(dummyNotary.party) {
unverifiedTransaction {
attachment(Cash.PROGRAM_ID)
output(Cash.PROGRAM_ID, "MEGA_CORP cash",
Cash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MEGA_CORP))
amount = 1000.DOLLARS `issued by` megaCorp.ref(1, 1),
owner = megaCorp.party))
}
transaction {
attachment(Cash.PROGRAM_ID)
input("MEGA_CORP cash")
output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<Cash.State>().copy(owner = AnonymousParty(ALICE_PUBKEY)))
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
output(Cash.PROGRAM_ID, "MEGA_CORP cash 2", "MEGA_CORP cash".output<Cash.State>().copy(owner = AnonymousParty(alice.publicKey)))
command(megaCorp.publicKey, Cash.Commands.Move())
this.verifies()
}
@ -837,8 +806,8 @@ class CashTests {
attachment(Cash.PROGRAM_ID)
input("MEGA_CORP cash")
// We send it to another pubkey so that the transaction is not identical to the previous one
output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output<Cash.State>().copy(owner = ALICE))
command(MEGA_CORP_PUBKEY, Cash.Commands.Move())
output(Cash.PROGRAM_ID, "MEGA_CORP cash 3", "MEGA_CORP cash".output<Cash.State>().copy(owner = alice.party))
command(megaCorp.publicKey, Cash.Commands.Move())
this.verifies()
}
this.fails()
@ -850,11 +819,11 @@ class CashTests {
@Test
fun multiSpend() {
val tx = TransactionBuilder(DUMMY_NOTARY)
val tx = TransactionBuilder(dummyNotary.party)
database.transaction {
val payments = listOf(
PartyAndAmount(miniCorpAnonymised, 400.DOLLARS),
PartyAndAmount(CHARLIE_ANONYMISED, 150.DOLLARS)
PartyAndAmount(charlie.party.anonymise(), 150.DOLLARS)
)
Cash.generateSpend(ourServices, tx, payments, ourServices.myInfo.singleIdentityAndCert())
}
@ -865,9 +834,9 @@ class CashTests {
assertEquals(320.DOLLARS, out(1).amount.withoutIssuer())
assertEquals(150.DOLLARS, out(2).amount.withoutIssuer())
assertEquals(30.DOLLARS, out(3).amount.withoutIssuer())
assertEquals(MINI_CORP, out(0).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(1).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(2).amount.token.issuer.party)
assertEquals(MEGA_CORP, out(3).amount.token.issuer.party)
assertEquals(miniCorp.party, out(0).amount.token.issuer.party)
assertEquals(megaCorp.party, out(1).amount.token.issuer.party)
assertEquals(megaCorp.party, out(2).amount.token.issuer.party)
assertEquals(megaCorp.party, out(3).amount.token.issuer.party)
}
}

View File

@ -46,9 +46,15 @@ import java.time.Clock
import java.util.*
fun makeTestIdentityService(vararg identities: PartyAndCertificate) = InMemoryIdentityService(identities, DEV_ROOT_CA.certificate)
/**
* A singleton utility that only provides a mock identity, key and storage service. However, this is sufficient for
* building chains of transactions and verifying them. It isn't sufficient for testing flows however.
* An implementation of [ServiceHub] that is designed for in-memory unit tests of contract validation logic. It has
* enough functionality to do tests of code that queries the vault, inserts to the vault, and constructs/checks
* transactions. However it isn't enough to test flows and other aspects of an app that require a network. For that
* you should investigate [MockNetwork].
*
* There are a variety of constructors that can be used to supply enough data to simulate a node. Each mock service hub
* must have at least an identity of its own. The other components have defaults that work in most situations.
*/
open class MockServices private constructor(
cordappLoader: CordappLoader,
@ -80,6 +86,7 @@ open class MockServices private constructor(
/**
* Makes database and mock services appropriate for unit tests.
*
* @param moreKeys a list of additional [KeyPair] instances to be used by [MockServices].
* @param identityService an instance of [IdentityServiceInternal], see [makeTestIdentityService].
* @param initialIdentity the first (typically sole) identity the services will represent.
@ -109,20 +116,68 @@ open class MockServices private constructor(
}
return Pair(database, mockService)
}
@JvmStatic
private fun getCallerPackage(): String {
// TODO: In Java 9 there's a new stack walker API that is better than this.
// The magic number '3' here is to chop off this method, an invisible bridge method generated by the
// compiler and then the c'tor itself.
return Throwable().stackTrace[3].className.split('.').dropLast(1).joinToString(".")
}
}
private constructor(cordappLoader: CordappLoader, identityService: IdentityServiceInternal, initialIdentity: TestIdentity, moreKeys: Array<out KeyPair>) : this(cordappLoader, MockTransactionStorage(), identityService, initialIdentity, moreKeys)
private constructor(cordappLoader: CordappLoader, identityService: IdentityServiceInternal,
initialIdentity: TestIdentity, moreKeys: Array<out KeyPair>)
: this(cordappLoader, MockTransactionStorage(), identityService, initialIdentity, moreKeys)
/**
* Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service
* (you can get one from [makeTestIdentityService]) and represents the given identity.
*/
@JvmOverloads
constructor(cordappPackages: List<String>, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentity: TestIdentity, vararg moreKeys: KeyPair) : this(CordappLoader.createWithTestPackages(cordappPackages), identityService, initialIdentity, moreKeys)
/**
* Create a mock [ServiceHub] that looks for app code in the given package names, uses the provided identity service
* (you can get one from [makeTestIdentityService]) and represents the given identity.
*/
@JvmOverloads
constructor(cordappPackages: List<String>, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name, key: KeyPair, vararg moreKeys: KeyPair) : this(cordappPackages, identityService, TestIdentity(initialIdentityName, key), *moreKeys)
/**
* Create a mock [ServiceHub] that can't load CorDapp code, which uses the provided identity service
* (you can get one from [makeTestIdentityService]) and which represents the given identity.
*/
@JvmOverloads
constructor(cordappPackages: List<String>, identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name) : this(cordappPackages, identityService, TestIdentity(initialIdentityName))
/**
* Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses the provided identity service
* (you can get one from [makeTestIdentityService]) and which represents the given identity.
*/
@JvmOverloads
constructor(cordappPackages: List<String>, identityService: IdentityServiceInternal = makeTestIdentityService(), vararg moreKeys: KeyPair) : this(cordappPackages, identityService, TestIdentity.fresh("MockServices"), *moreKeys)
constructor(identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name, key: KeyPair, vararg moreKeys: KeyPair)
: this(listOf(getCallerPackage()), identityService, TestIdentity(initialIdentityName, key), *moreKeys)
/**
* Create a mock [ServiceHub] which uses the package of the caller to find CorDapp code. It uses the provided identity service
* (you can get one from [makeTestIdentityService]) and which represents the given identity. It has no keys.
*/
@JvmOverloads
constructor(identityService: IdentityServiceInternal = makeTestIdentityService(), initialIdentityName: CordaX500Name)
: this(listOf(getCallerPackage()), identityService, TestIdentity(initialIdentityName))
/**
* A helper constructor that requires at least one test identity to be registered, and which takes the package of
* the caller as the package in which to find app code. This is the most convenient constructor and the one that
* is normally worth using. The first identity is the identity of this service hub, the rest are identities that
* it is aware of.
*/
constructor(firstIdentity: TestIdentity, vararg moreIdentities: TestIdentity) : this(
listOf(getCallerPackage()),
makeTestIdentityService(*listOf(firstIdentity, *moreIdentities).map { it.identity }.toTypedArray()),
firstIdentity, firstIdentity.keyPair
)
override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable<SignedTransaction>) {
txs.forEach {
@ -185,7 +240,7 @@ class MockKeyManagementService(val identityService: IdentityServiceInternal,
private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey))
private fun getSigningKeyPair(publicKey: PublicKey): KeyPair {
val pk = publicKey.keys.first { keyStore.containsKey(it) }
val pk = publicKey.keys.firstOrNull { keyStore.containsKey(it) } ?: throw IllegalArgumentException("Public key not found: ${publicKey.toStringShort()}")
return KeyPair(pk, keyStore[pk]!!)
}