From ef2ff777a70f40e2dcb9ab7ef03dd1a3023ee4a6 Mon Sep 17 00:00:00 2001 From: Jose Coll Date: Wed, 19 Oct 2016 10:38:14 +0100 Subject: [PATCH] Adapted all Contract tests that perform Cash Spending to use the Vault Service. Note: pending resolution of 2 failing tests (IRSSimulation, CP issue, move & redeem) --- .../contracts/JavaCommercialPaper.java | 5 +- .../com/r3corda/contracts/CommercialPaper.kt | 5 +- .../contracts/CommercialPaperLegacy.kt | 6 +- .../r3corda/contracts/asset/OnLedgerAsset.kt | 19 -- .../clause/AbstractConserveAmount.kt | 84 --------- .../r3corda/contracts/testing/VaultFiller.kt | 10 +- .../r3corda/contracts/CommercialPaperTests.kt | 137 ++++++++------ .../com/r3corda/contracts/asset/CashTests.kt | 169 ++++++++++++++---- 8 files changed, 236 insertions(+), 199 deletions(-) diff --git a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java index ca850ecf14..d344bd2b9f 100644 --- a/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java +++ b/contracts/src/main/java/com/r3corda/contracts/JavaCommercialPaper.java @@ -7,6 +7,7 @@ import com.r3corda.core.contracts.Timestamp; import com.r3corda.core.contracts.TransactionForContract.*; import com.r3corda.core.contracts.clauses.*; import com.r3corda.core.crypto.*; +import com.r3corda.core.node.services.*; import com.r3corda.core.transactions.*; import kotlin.*; import org.jetbrains.annotations.*; @@ -304,8 +305,8 @@ public class JavaCommercialPaper implements Contract { return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); } - public void generateRedeem(TransactionBuilder tx, StateAndRef paper, List> vault) throws InsufficientBalanceException { - new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), vault, null); + public void generateRedeem(TransactionBuilder tx, StateAndRef paper, VaultService vault) throws InsufficientBalanceException { + vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null); tx.addInputState(paper); tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner())); } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt index f2aae1d7ce..f157849159 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaper.kt @@ -14,6 +14,7 @@ import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toBase58String import com.r3corda.core.crypto.toStringShort +import com.r3corda.core.node.services.VaultService import com.r3corda.core.random63BitValue import com.r3corda.core.schemas.MappedSchema import com.r3corda.core.schemas.PersistentState @@ -218,10 +219,10 @@ class CommercialPaper : Contract { * @throws InsufficientBalanceException if the vault doesn't contain enough money to pay the redeemer. */ @Throws(InsufficientBalanceException::class) - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: List>) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { // Add the cash movement using the states in our vault. val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } - Cash().generateSpend(tx, amount, paper.state.data.owner, vault) + vault.generateSpend(tx, amount, paper.state.data.owner) tx.addInputState(paper) tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt index fc7a6c71c6..cc97eb7637 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/CommercialPaperLegacy.kt @@ -9,6 +9,7 @@ import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.toStringShort import com.r3corda.core.node.services.Vault +import com.r3corda.core.node.services.VaultService import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.core.utilities.Emoji import java.security.PublicKey @@ -125,10 +126,9 @@ class CommercialPaperLegacy : Contract { } @Throws(InsufficientBalanceException::class) - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: Vault) { + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { // Add the cash movement using the states in our vault. - Cash().generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), - paper.state.data.owner, vault.statesOfType()) + vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) tx.addInputState(paper) tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner)) } diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt index 273df7bbec..9c97b1cc60 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/asset/OnLedgerAsset.kt @@ -47,25 +47,6 @@ abstract class OnLedgerAsset> : Co generateExitCommand = { amount -> generateExitCommand(amount) } ) - - /** - * Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey. - * Note that the vault is not updated: it's up to you to do that. - * - * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of asset claims they are willing to accept. - */ - @Throws(InsufficientBalanceException::class) - fun generateSpend(tx: TransactionBuilder, - amount: Amount, - to: PublicKey, - assetsStates: List>, - onlyFromParties: Set? = null): List - = conserveClause.generateSpend(tx, amount, to, assetsStates, onlyFromParties, - deriveState = { state, amount, owner -> deriveState(state, amount, owner) }, - generateMoveCommand = { generateMoveCommand() }) - abstract fun generateExitCommand(amount: Amount>): FungibleAsset.Commands.Exit abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue abstract fun generateMoveCommand(): FungibleAsset.Commands.Move diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt index b8e01b2504..3313ff4215 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/clause/AbstractConserveAmount.kt @@ -85,90 +85,6 @@ abstract class AbstractConserveAmount, C : CommandData, T : return amountIssued.token.issuer.party.owningKey } - /** - * Generate a transaction that consumes one or more of the given input states to move assets to the given pubkey. - * Note that the vault is not updated: it's up to you to do that. - * - * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set - * of given parties. This can be useful if the party you're trying to pay has expectations - * about which type of asset claims they are willing to accept. - */ - @Throws(InsufficientBalanceException::class) - fun generateSpend(tx: TransactionBuilder, - amount: Amount, - to: PublicKey, - assetsStates: List>, - onlyFromParties: Set? = null, - deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, - generateMoveCommand: () -> CommandData): List { - // Discussion - // - // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. - // - // First we must select a set of asset states (which for convenience we will call 'coins' here, as in bitcoinj). - // The input states can be considered our "vault", and may consist of different products, and with different - // issuers and deposits. - // - // Coin selection is a complex problem all by itself and many different approaches can be used. It is easily - // possible for different actors to use different algorithms and approaches that, for example, compete on - // privacy vs efficiency (number of states created). Some spends may be artificial just for the purposes of - // obfuscation and so on. - // - // Having selected input states of the correct asset, we must craft output states for the amount we're sending and - // the "change", which goes back to us. The change is required to make the amounts balance. We may need more - // than one change output in order to avoid merging assets from different deposits. The point of this design - // is to ensure that ledger entries are immutable and globally identifiable. - // - // Finally, we add the states to the provided partial transaction. - - val currency = amount.token - var acceptableCoins = run { - val ofCurrency = assetsStates.filter { it.state.data.amount.token.product == currency } - if (onlyFromParties != null) - ofCurrency.filter { it.state.data.deposit.party in onlyFromParties } - else - ofCurrency - } - tx.notary = acceptableCoins.firstOrNull()?.state?.notary - // TODO: We should be prepared to produce multiple transactions spending inputs from - // different notaries, or at least group states by notary and take the set with the - // highest total value - acceptableCoins = acceptableCoins.filter { it.state.notary == tx.notary } - - val (gathered, gatheredAmount) = gatherCoins(acceptableCoins, amount) - val takeChangeFrom = gathered.firstOrNull() - val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.issuanceDef) - } else { - null - } - val keysUsed = gathered.map { it.state.data.owner }.toSet() - - val states = gathered.groupBy { it.state.data.deposit }.map { - val coins = it.value - val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() - deriveState(coins.first().state, totalAmount, to) - } - - val outputs = if (change != null) { - // Just copy a key across as the change key. In real life of course, this works but leaks private data. - // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow - // value flows through the transaction graph. - val changeKey = gathered.first().state.data.owner - // Add a change output and adjust the last output downwards. - states.subList(0, states.lastIndex) + - states.last().let { deriveState(it, it.data.amount - change, it.data.owner) } + - deriveState(gathered.last().state, change, changeKey) - } else states - - for (state in gathered) tx.addInputState(state) - for (state in outputs) tx.addOutputState(state) - // What if we already have a move command with the right keys? Filter it out here or in platform code? - val keysList = keysUsed.toList() - tx.addCommand(generateMoveCommand(), keysList) - return keysList - } - override fun verify(tx: TransactionForContract, inputs: List, outputs: List, diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt b/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt index 21e2e2ee4c..3d28dd4e72 100644 --- a/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt +++ b/contracts/src/main/kotlin/com/r3corda/contracts/testing/VaultFiller.kt @@ -6,6 +6,7 @@ import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Issued +import com.r3corda.core.contracts.PartyAndReference import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.contracts.TransactionType import com.r3corda.core.crypto.Party @@ -14,6 +15,7 @@ import com.r3corda.core.node.services.Vault import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.utilities.DUMMY_NOTARY +import java.security.KeyPair import java.security.PublicKey import java.util.* @@ -34,7 +36,9 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, atMostThisManyStates: Int = 10, rng: Random = Random(), ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })), - ownedBy: PublicKey? = null): Vault { + ownedBy: PublicKey? = null, + issuedBy: PartyAndReference = DUMMY_CASH_ISSUER, + issuerKey: KeyPair = DUMMY_CASH_ISSUER_KEY): Vault { val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng) val myKey: PublicKey = ownedBy ?: myInfo.legalIdentity.owningKey @@ -43,8 +47,8 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, val cash = Cash() val transactions: List = amounts.map { pennies -> val issuance = TransactionType.General.Builder(null) - cash.generateIssue(issuance, Amount(pennies, Issued(DUMMY_CASH_ISSUER.copy(reference = ref), howMuch.token)), myKey, outputNotary) - issuance.signWith(DUMMY_CASH_ISSUER_KEY) + cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), myKey, outputNotary) + issuance.signWith(issuerKey) return@map issuance.toSignedTransaction(true) } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt index 8c4e3ec3c6..7b135ced29 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/CommercialPaperTests.kt @@ -7,6 +7,7 @@ import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash import com.r3corda.core.days import com.r3corda.core.node.recordTransactions +import com.r3corda.core.node.services.VaultService import com.r3corda.core.seconds import com.r3corda.core.transactions.LedgerTransaction import com.r3corda.core.transactions.SignedTransaction @@ -14,8 +15,12 @@ import com.r3corda.core.utilities.DUMMY_NOTARY import com.r3corda.core.utilities.DUMMY_NOTARY_KEY import com.r3corda.core.utilities.DUMMY_PUBKEY_1 import com.r3corda.core.utilities.TEST_TX_TIME +import com.r3corda.node.services.vault.NodeVaultService +import com.r3corda.node.utilities.configureDatabase +import com.r3corda.node.utilities.databaseTransaction import com.r3corda.testing.node.MockServices import com.r3corda.testing.* +import com.r3corda.testing.node.makeTestDataSourceProperties import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -199,62 +204,90 @@ class CommercialPaperTestsGeneric { @Test fun `issue move and then redeem`() { - val aliceServices = MockServices() - val alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS) - val bigCorpServices = MockServices() - val bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS) + val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties()) + val database = dataSourceAndDatabase.second + databaseTransaction(database) { - // Propagate the cash transactions to each side. - aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) - bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + val aliceServices = object : MockServices() { + override val vaultService: VaultService = NodeVaultService(this) - // 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.legalIdentity.ref(1) - val issueTX: SignedTransaction = - CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, 30.seconds) - signWith(bigCorpServices.key) - signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + override fun recordTransactions(txs: Iterable) { + for (stx in txs) { + storageService.validatedTransactions.addTransaction(stx) + } + // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. + vaultService.notifyAll(txs.map { it.tx }) + } + } + val alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) + val aliceVaultService = aliceServices.vaultService - // Alice pays $9000 to BigCorp to own some of their debt. - val moveTX: SignedTransaction = run { - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesVault.statesOfType()) - CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public) - ptx.signWith(bigCorpServices.key) - ptx.signWith(aliceServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - ptx.toSignedTransaction() + val bigCorpServices = object : MockServices() { + override val vaultService: VaultService = NodeVaultService(this) + + override fun recordTransactions(txs: Iterable) { + for (stx in txs) { + storageService.validatedTransactions.addTransaction(stx) + } + // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. + vaultService.notifyAll(txs.map { it.tx }) + } + } + val bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1) + val bigCorpVaultService = bigCorpServices.vaultService + + // Propagate the cash transactions to each side. + aliceServices.recordTransactions(bigCorpVault.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! }) + bigCorpServices.recordTransactions(alicesVault.states.map { aliceServices.storageService.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.legalIdentity.ref(1) + val issueTX: SignedTransaction = + CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { + setTime(TEST_TX_TIME, 30.seconds) + signWith(bigCorpServices.key) + signWith(DUMMY_NOTARY_KEY) + }.toSignedTransaction() + + // Alice pays $9000 to BigCorp to own some of their debt. + val moveTX: SignedTransaction = run { + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) + aliceVaultService.generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public) + CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public) + ptx.signWith(bigCorpServices.key) + ptx.signWith(aliceServices.key) + ptx.signWith(DUMMY_NOTARY_KEY) + ptx.toSignedTransaction() + } + + fun makeRedeemTX(time: Instant): SignedTransaction { + val ptx = TransactionType.General.Builder(DUMMY_NOTARY) + ptx.setTime(time, 30.seconds) + CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVaultService) + ptx.signWith(aliceServices.key) + ptx.signWith(bigCorpServices.key) + ptx.signWith(DUMMY_NOTARY_KEY) + return ptx.toSignedTransaction() + } + + val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) + val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) + + // Verify the txns are valid and insert into both sides. + listOf(issueTX, moveTX).forEach { + it.toLedgerTransaction(aliceServices).verify() + aliceServices.recordTransactions(it) + bigCorpServices.recordTransactions(it) + } + + val e = assertFailsWith(TransactionVerificationException::class) { + tooEarlyRedemption.toLedgerTransaction(aliceServices).verify() + } + assertTrue(e.cause!!.message!!.contains("paper must have matured")) + + validRedemption.toLedgerTransaction(aliceServices).verify() } - - fun makeRedeemTX(time: Instant): SignedTransaction { - val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - ptx.setTime(time, 30.seconds) - CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVault.statesOfType()) - ptx.signWith(aliceServices.key) - ptx.signWith(bigCorpServices.key) - ptx.signWith(DUMMY_NOTARY_KEY) - return ptx.toSignedTransaction() - } - - val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days) - val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days) - - // Verify the txns are valid and insert into both sides. - listOf(issueTX, moveTX).forEach { - it.toLedgerTransaction(aliceServices).verify() - aliceServices.recordTransactions(it) - bigCorpServices.recordTransactions(it) - } - - val e = assertFailsWith(TransactionVerificationException::class) { - tooEarlyRedemption.toLedgerTransaction(aliceServices).verify() - } - assertTrue(e.cause!!.message!!.contains("paper must have matured")) - - validRedemption.toLedgerTransaction(aliceServices).verify() } } diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt index 88ad5ac9b8..9c7d35f9b9 100644 --- a/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt +++ b/contracts/src/test/kotlin/com/r3corda/contracts/asset/CashTests.kt @@ -1,15 +1,32 @@ package com.r3corda.contracts.asset +import com.r3corda.contracts.testing.fillWithSomeTestCash import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.SecureHash +import com.r3corda.core.crypto.generateKeyPair +import com.r3corda.core.node.services.Vault +import com.r3corda.core.node.services.VaultService import com.r3corda.core.serialization.OpaqueBytes +import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.WireTransaction import com.r3corda.core.utilities.DUMMY_NOTARY import com.r3corda.core.utilities.DUMMY_PUBKEY_1 import com.r3corda.core.utilities.DUMMY_PUBKEY_2 +import com.r3corda.core.utilities.LogHelper +import com.r3corda.node.services.vault.NodeVaultService +import com.r3corda.node.utilities.configureDatabase +import com.r3corda.node.utilities.databaseTransaction import com.r3corda.testing.* +import com.r3corda.testing.node.MockKeyManagementService +import com.r3corda.testing.node.MockServices +import com.r3corda.testing.node.makeTestDataSourceProperties +import org.jetbrains.exposed.sql.Database +import org.junit.After +import org.junit.Before import org.junit.Test +import java.io.Closeable +import java.security.KeyPair import java.security.PublicKey import java.util.* import kotlin.test.* @@ -29,6 +46,52 @@ class CashTests { amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref)))) ) + lateinit var services: MockServices + val vault: VaultService get() = services.vaultService + lateinit var dataSource: Closeable + lateinit var database: Database + lateinit var VAULT: Vault + + @Before + fun setUp() { + LogHelper.setLevel(NodeVaultService::class) + val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties()) + dataSource = dataSourceAndDatabase.first + database = dataSourceAndDatabase.second + databaseTransaction(database) { + services = object : MockServices() { + + override val keyManagementService: MockKeyManagementService = MockKeyManagementService(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) + override val vaultService: VaultService = NodeVaultService(this) + + override fun recordTransactions(txs: Iterable) { + for (stx in txs) { + storageService.validatedTransactions.addTransaction(stx) + } + // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. + vaultService.notifyAll(txs.map { it.tx }) + } + } + + services.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1) + services.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1) + services.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1) + services.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1, + issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1) + + VAULT = services.vaultService.currentVault + } + } + + @After + fun tearDown() { + LogHelper.reset(NodeVaultService::class) + dataSource.close() + } + @Test fun trivial() { transaction { @@ -402,7 +465,9 @@ class CashTests { // // Spend tx generation - val OUR_PUBKEY_1 = DUMMY_PUBKEY_1 + val OUR_KEY: KeyPair by lazy { generateKeyPair() } + val OUR_PUBKEY_1: PublicKey get() = OUR_KEY.public + val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = @@ -428,8 +493,10 @@ class CashTests { } fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction { - val tx = TransactionType.General.Builder(DUMMY_NOTARY) - Cash().generateSpend(tx, amount, dest, WALLET) + var tx = TransactionType.General.Builder(DUMMY_NOTARY) + databaseTransaction(database) { + vault.generateSpend(tx, amount, dest) + } return tx.toWireTransaction() } @@ -485,58 +552,92 @@ class CashTests { @Test fun generateSimpleDirectSpend() { - val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + + val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) + + val vaultState = VAULT.states.elementAt(0) as StateAndRef + assertEquals(vaultState.ref, wtx.inputs[0]) + assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data) + assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + } } @Test fun generateSimpleSpendWithParties() { - val tx = TransactionType.General.Builder(DUMMY_NOTARY) - Cash().generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, WALLET, setOf(MINI_CORP)) - assertEquals(WALLET[2].ref, tx.inputStates()[0]) + + databaseTransaction(database) { + + val tx = TransactionType.General.Builder(DUMMY_NOTARY) + vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP)) + + assertEquals(VAULT.states.elementAt(2).ref, tx.inputStates()[0]) + } } @Test fun generateSimpleSpendWithChange() { - val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) - assertEquals(WALLET[0].state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + + val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) + + val vaultState = VAULT.states.elementAt(0) as StateAndRef + assertEquals(vaultState.ref, wtx.inputs[0]) + assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(vaultState.state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) + assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + } } @Test fun generateSpendWithTwoInputs() { - val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) - assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[1].ref, wtx.inputs[1]) - assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) + + val vaultState0 = VAULT.states.elementAt(0) as StateAndRef + val vaultState1 = VAULT.states.elementAt(1) + assertEquals(vaultState0.ref, wtx.inputs[0]) + assertEquals(vaultState1.ref, wtx.inputs[1]) + assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + } } @Test fun generateSpendMixedDeposits() { - val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) - assertEquals(3, wtx.inputs.size) - assertEquals(WALLET[0].ref, wtx.inputs[0]) - assertEquals(WALLET[1].ref, wtx.inputs[1]) - assertEquals(WALLET[2].ref, wtx.inputs[2]) - assertEquals(WALLET[0].state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) - assertEquals(WALLET[2].state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + + databaseTransaction(database) { + val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) + assertEquals(3, wtx.inputs.size) + + val vaultState0 = VAULT.states.elementAt(0) as StateAndRef + val vaultState1 = VAULT.states.elementAt(1) + val vaultState2 = VAULT.states.elementAt(2) as StateAndRef + assertEquals(vaultState0.ref, wtx.inputs[0]) + assertEquals(vaultState1.ref, wtx.inputs[1]) + assertEquals(vaultState2.ref, wtx.inputs[2]) + assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(vaultState2.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[1].data) + assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + } } @Test fun generateSpendInsufficientBalance() { - val e: InsufficientBalanceException = assertFailsWith("balance") { - makeSpend(1000.DOLLARS, THEIR_PUBKEY_1) - } - assertEquals((1000 - 580).DOLLARS, e.amountMissing) - assertFailsWith(InsufficientBalanceException::class) { - makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1) + databaseTransaction(database) { + + val e: InsufficientBalanceException = assertFailsWith("balance") { + makeSpend(1000.DOLLARS, THEIR_PUBKEY_1) + } + assertEquals((1000 - 580).DOLLARS, e.amountMissing) + + assertFailsWith(InsufficientBalanceException::class) { + makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1) + } } }