diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index ac24b0a2f5..c39694b5e7 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -145,6 +145,8 @@ which is then referenced within a custom flow: :start-after: DOCSTART TopupIssuer :end-before: DOCEND TopupIssuer +For examples on testing ``@CordaService`` implementations, see the oracle example :doc:`here ` + .. _database_migration_ref: Database Migration diff --git a/docs/source/api-testing.rst b/docs/source/api-testing.rst index d8a2511ccc..6a04804b88 100644 --- a/docs/source/api-testing.rst +++ b/docs/source/api-testing.rst @@ -59,6 +59,8 @@ object, or by using named paramters in Kotlin: .. sourcecode:: kotlin val network = MockNetwork( + // A list of packages to scan. Any contracts, flows and Corda services within these + // packages will be automatically available to any nodes within the mock network cordappPackages = listOf("my.cordapp.package", "my.other.cordapp.package"), // If true then each node will be run in its own thread. This can result in race conditions in your // code if not carefully written, but is more realistic and may help if you have flows in your app that @@ -77,7 +79,10 @@ object, or by using named paramters in Kotlin: // notary implementations. servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.Random()) - val network2 = MockNetwork(listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters( + val network2 = MockNetwork( + // A list of packages to scan. Any contracts, flows and Corda services within these + // packages will be automatically available to any nodes within the mock network + listOf("my.cordapp.package", "my.other.cordapp.package"), MockNetworkParameters( // If true then each node will be run in its own thread. This can result in race conditions in your // code if not carefully written, but is more realistic and may help if you have flows in your app that // do long blocking operations. @@ -98,7 +103,10 @@ object, or by using named paramters in Kotlin: .. sourcecode:: java - MockNetwork network = MockNetwork(ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"), + MockNetwork network = MockNetwork( + // A list of packages to scan. Any contracts, flows and Corda services within these + // packages will be automatically available to any nodes within the mock network + ImmutableList.of("my.cordapp.package", "my.other.cordapp.package"), new MockNetworkParameters() // If true then each node will be run in its own thread. This can result in race conditions in // your code if not carefully written, but is more realistic and may help if you have flows in @@ -294,6 +302,7 @@ Further examples ^^^^^^^^^^^^^^^^ * See the flow testing tutorial :doc:`here ` +* See the oracle tutorial :doc:`here ` for information on testing ``@CordaService`` classes * Further examples are available in the Example CorDapp in `Java `_ and `Kotlin `_ diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index 7c1fa36f53..5066fe2f93 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -269,7 +269,26 @@ Here's an example of it in action from ``FixingFlow.Fixer``. Testing ------- -When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode`` -is a simplified node suitable for tests. One feature that isn't available (and which is not suitable for unit testing -anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when -working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node. \ No newline at end of file +The ``MockNetwork`` allows the creation of ``MockNode`` instances, which are simplified nodes which can be used for +testing (see :doc:`api-testing`). When creating the ``MockNetwork`` you supply a list of packages to scan for CorDapps. +Make sure the packages you provide include your oracle service, and it automatically be installed in the test nodes. +Then you can create an oracle node on the ``MockNetwork`` and insert any initialisation logic you want to use. In this +case, our ``Oracle`` service is in the ``net.corda.irs.api`` package, so the following test setup will install +the service in each node. Then an oracle node with an oracle service which is initialised with some data is created on +the mock network: + +.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 + +You can then write tests on your mock network to verify the nodes interact with your Oracle correctly. + +.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + :dedent: 4 + +See `here `_ for more examples. diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index d589a6c8a6..62381d8272 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -58,6 +58,61 @@ interface NetworkMapCacheBaseInternal : NetworkMapCacheBase { interface ServiceHubInternal : ServiceHub { companion object { private val log = contextLogger() + + fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable, + validatedTransactions: WritableTransactionStorage, + stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage, + vaultService: VaultServiceInternal) { + + require(txs.any()) { "No transactions passed in for recording" } + val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } + val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id + if (stateMachineRunId != null) { + recordedTransactions.forEach { + stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id) + } + } else { + log.warn("Transactions recorded from outside of a state machine") + } + + if (statesToRecord != StatesToRecord.NONE) { + // When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states + // that do not involve us and that we cannot sign for. This will break coin selection and thus a warning + // is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net). + // + // The reason for this is three-fold: + // + // 1) We are putting in place the observer mode feature relatively quickly to meet specific customer + // launch target dates. + // + // 2) The right design for vaults which mix observations and relevant states isn't entirely clear yet. + // + // 3) If we get the design wrong it could create security problems and business confusions. + // + // Back in the bitcoinj days I did add support for "watching addresses" to the wallet code, which is the + // Bitcoin equivalent of observer nodes: + // + // https://bitcoinj.github.io/working-with-the-wallet#watching-wallets + // + // The ability to have a wallet containing both irrelevant and relevant states complicated everything quite + // dramatically, even methods as basic as the getBalance() API which required additional modes to let you + // query "balance I can spend" vs "balance I am observing". In the end it might have been better to just + // require the user to create an entirely separate wallet for observing with. + // + // In Corda we don't support a single node having multiple vaults (at the time of writing), and it's not + // clear that's the right way to go: perhaps adding an "origin" column to the VAULT_STATES table is a better + // solution. Then you could select subsets of states depending on where the report came from. + // + // The risk of doing this is that apps/developers may use 'canned SQL queries' not written by us that forget + // to add a WHERE clause for the origin column. Those queries will seem to work most of the time until + // they're run on an observer node and mix in irrelevant data. In the worst case this may result in + // erroneous data being reported to the user, which could cause security problems. + // + // Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely + // to make writes to the ledger very often or at all, we choose to punt this issue for the time being. + vaultService.notifyAll(statesToRecord, recordedTransactions.map { it.coreTransaction }) + } + } } override val vaultService: VaultServiceInternal @@ -80,54 +135,7 @@ interface ServiceHubInternal : ServiceHub { val networkMapUpdater: NetworkMapUpdater override val cordappProvider: CordappProviderInternal override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { - require(txs.any()) { "No transactions passed in for recording" } - val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } - val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id - if (stateMachineRunId != null) { - recordedTransactions.forEach { - stateMachineRecordedTransactionMapping.addMapping(stateMachineRunId, it.id) - } - } else { - log.warn("Transactions recorded from outside of a state machine") - } - - if (statesToRecord != StatesToRecord.NONE) { - // When the user has requested StatesToRecord.ALL we may end up recording and relationally mapping states - // that do not involve us and that we cannot sign for. This will break coin selection and thus a warning - // is present in the documentation for this feature (see the "Observer nodes" tutorial on docs.corda.net). - // - // The reason for this is three-fold: - // - // 1) We are putting in place the observer mode feature relatively quickly to meet specific customer - // launch target dates. - // - // 2) The right design for vaults which mix observations and relevant states isn't entirely clear yet. - // - // 3) If we get the design wrong it could create security problems and business confusions. - // - // Back in the bitcoinj days I did add support for "watching addresses" to the wallet code, which is the - // Bitcoin equivalent of observer nodes: - // - // https://bitcoinj.github.io/working-with-the-wallet#watching-wallets - // - // The ability to have a wallet containing both irrelevant and relevant states complicated everything quite - // dramatically, even methods as basic as the getBalance() API which required additional modes to let you - // query "balance I can spend" vs "balance I am observing". In the end it might have been better to just - // require the user to create an entirely separate wallet for observing with. - // - // In Corda we don't support a single node having multiple vaults (at the time of writing), and it's not - // clear that's the right way to go: perhaps adding an "origin" column to the VAULT_STATES table is a better - // solution. Then you could select subsets of states depending on where the report came from. - // - // The risk of doing this is that apps/developers may use 'canned SQL queries' not written by us that forget - // to add a WHERE clause for the origin column. Those queries will seem to work most of the time until - // they're run on an observer node and mix in irrelevant data. In the worst case this may result in - // erroneous data being reported to the user, which could cause security problems. - // - // Because the primary use case for recording irrelevant states is observer/regulator nodes, who are unlikely - // to make writes to the ledger very often or at all, we choose to punt this issue for the time being. - vaultService.notifyAll(statesToRecord, txs.map { it.coreTransaction }) - } + recordTransactions(statesToRecord, txs, validatedTransactions, stateMachineRecordedTransactionMapping, vaultService) } fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index d439a72c7c..88420afbeb 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -140,6 +140,18 @@ class NodeVaultServiceTest { return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java) } + @Test + fun `duplicate insert of transaction does not fail`() { + database.transaction { + val cash = Cash() + val howMuch = 100.DOLLARS + val issuance = TransactionBuilder(null as Party?) + cash.generateIssue(issuance, Amount(howMuch.quantity, Issued(DUMMY_CASH_ISSUER, howMuch.token)), services.myInfo.singleIdentity(), dummyNotary.party) + val transaction = issuerServices.signInitialTransaction(issuance, DUMMY_CASH_ISSUER.party.owningKey) + services.recordTransactions(transaction) + services.recordTransactions(transaction) + } + } @Test fun `states not local to instance`() { diff --git a/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index 75a88974ff..5bb093319f 100644 --- a/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -79,6 +79,8 @@ object FixingFlow { @Suspendable override fun filtering(elem: Any): Boolean { return when (elem) { + // Only expose Fix commands in which the oracle is on the list of requested signers + // to the oracle node, to avoid leaking privacy is Command<*> -> handshake.payload.oracle.owningKey in elem.signers && elem.value is Fix else -> false } @@ -91,7 +93,7 @@ object FixingFlow { } /** - * One side of the fixing flow for an interest rate swap, but could easily be generalised furher. + * One side of the fixing flow for an interest rate swap, but could easily be generalised further. * * As per the [Fixer], do not infer too much from this class name in terms of business roles. This * is just the "side" of the flow run by the party with the floating leg as a way of deciding who diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 7cdb215e71..fa68ac46be 100644 --- a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -15,22 +15,15 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionState import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.CordaX500Name -import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.contracts.Fix -import net.corda.finance.contracts.FixOf import net.corda.finance.contracts.asset.CASH import net.corda.finance.contracts.asset.Cash -import net.corda.irs.flows.RatesFixFlow import net.corda.node.internal.configureDatabase import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.testing.core.* -import net.corda.testing.internal.withoutTestSerialization -import net.corda.testing.internal.LogHelper import net.corda.testing.internal.rigorousMock import net.corda.testing.node.* import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties @@ -220,50 +213,10 @@ class NodeInterestRatesTest { assertFailsWith { oracle.sign(ftx) } // It throws failed requirement (as it is empty there is no command to check and sign). } - @Test - fun `network tearoff`() = withoutTestSerialization { - val mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs")) - val aliceNode = mockNet.createPartyNode(ALICE_NAME) - val oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply { - registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) - registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) - database.transaction { - services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA - } - } - val oracle = oracleNode.services.myInfo.singleIdentity() - val tx = makePartialTX() - val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") - val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1")) - LogHelper.setLevel("rates") - mockNet.runNetwork() - val future = aliceNode.startFlow(flow) - mockNet.runNetwork() - future.getOrThrow() - // We should now have a valid fix of our tx from the oracle. - val fix = tx.toWireTransaction(services).commands.map { it.value as Fix }.first() - assertEquals(fixOf, fix.of) - assertEquals(BigDecimal("0.678"), fix.value) - mockNet.stopNodes() - } - - class FilteredRatesFlow(tx: TransactionBuilder, - oracle: Party, - fixOf: FixOf, - expectedRate: BigDecimal, - rateTolerance: BigDecimal, - progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) - : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) { - override fun filtering(elem: Any): Boolean { - return when (elem) { - is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix - else -> false - } - } - } - private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems( TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy ALICE, Cash.PROGRAM_ID, DUMMY_NOTARY)) private fun makeFullTx() = makePartialTX().withItems(dummyCommand()) } + + diff --git a/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt new file mode 100644 index 0000000000..4c27556e87 --- /dev/null +++ b/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt @@ -0,0 +1,155 @@ +package net.corda.irs.api + +import com.google.common.collect.testing.Helpers +import com.google.common.collect.testing.Helpers.assertContains +import net.corda.core.contracts.Command +import net.corda.core.contracts.TransactionState +import net.corda.core.flows.UnexpectedFlowEndException +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.Fix +import net.corda.finance.contracts.FixOf +import net.corda.finance.contracts.asset.CASH +import net.corda.finance.contracts.asset.Cash +import net.corda.irs.flows.RatesFixFlow +import net.corda.testing.core.* +import net.corda.testing.internal.LogHelper +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.StartedMockNode +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.math.BigDecimal +import kotlin.test.assertEquals + +class OracleNodeTearOffTests { + private val TEST_DATA = NodeInterestRates.parseFile(""" + LIBOR 2016-03-16 1M = 0.678 + LIBOR 2016-03-16 2M = 0.685 + LIBOR 2016-03-16 1Y = 0.890 + LIBOR 2016-03-16 2Y = 0.962 + EURIBOR 2016-03-15 1M = 0.123 + EURIBOR 2016-03-15 2M = 0.111 + """.trimIndent()) + + private val dummyCashIssuer = TestIdentity(CordaX500Name("Cash issuer", "London", "GB")) + + val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val alice = TestIdentity(ALICE_NAME, 70) + private lateinit var mockNet: MockNetwork + private lateinit var aliceNode: StartedMockNode + private lateinit var oracleNode: StartedMockNode + private val oracle get() = oracleNode.services.myInfo.singleIdentity() + + @Before + // DOCSTART 1 + fun setUp() { + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs")) + aliceNode = mockNet.createPartyNode(ALICE_NAME) + oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply { + transaction { + services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA + } + } + } + // DOCEND 1 + + @After + fun tearDown() { + mockNet.stopNodes() + } + + // DOCSTART 2 + @Test + fun `verify that the oracle signs the transaction if the interest rate within allowed limit`() { + // Create a partial transaction + val tx = TransactionBuilder(DUMMY_NOTARY) + .withItems(TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY)) + // Specify the rate we wish to get verified by the oracle + val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") + + // Create a new flow for the fix + val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1")) + // Run the mock network and wait for a result + mockNet.runNetwork() + val future = aliceNode.startFlow(flow) + mockNet.runNetwork() + future.getOrThrow() + + // We should now have a valid rate on our tx from the oracle. + val fix = tx.toWireTransaction(aliceNode.services).commands.map { it }.first() + assertEquals(fixOf, (fix.value as Fix).of) + // Check that the response contains the valid rate, which is within the supplied tolerance + assertEquals(BigDecimal("0.678"), (fix.value as Fix).value) + // Check that the transaction has been signed by the oracle + assertContains(fix.signers, oracle.owningKey) + } + // DOCEND 2 + + @Test + fun `verify that the oracle rejects the transaction if the interest rate is outside the allowed limit`() { + val tx = makePartialTX() + val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") + val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.695"), BigDecimal("0.01")) + LogHelper.setLevel("rates") + + mockNet.runNetwork() + val future = aliceNode.startFlow(flow) + mockNet.runNetwork() + assertThatThrownBy{ + future.getOrThrow() + }.isInstanceOf(RatesFixFlow.FixOutOfRange::class.java).hasMessage("Fix out of range by 0.017") + } + + @Test + fun `verify that the oracle rejects the transaction if there is a privacy leak`() { + val tx = makePartialTX() + val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") + val flow = OverFilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1")) + LogHelper.setLevel("rates") + + mockNet.runNetwork() + val future = aliceNode.startFlow(flow) + mockNet.runNetwork() + //The oracle + assertThatThrownBy{ + future.getOrThrow() + }.isInstanceOf(UnexpectedFlowEndException::class.java) + } + + // Creates a version of [RatesFixFlow] that makes the command + class FilteredRatesFlow(tx: TransactionBuilder, + oracle: Party, + fixOf: FixOf, + expectedRate: BigDecimal, + rateTolerance: BigDecimal, + progressTracker: ProgressTracker = tracker(fixOf.name)) + : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) { + override fun filtering(elem: Any): Boolean { + return when (elem) { + is Command<*> -> oracle.owningKey in elem.signers && elem.value is Fix + else -> false + } + } + } + + // Creates a version of [RatesFixFlow] that makes the command + class OverFilteredRatesFlow(tx: TransactionBuilder, + oracle: Party, + fixOf: FixOf, + expectedRate: BigDecimal, + rateTolerance: BigDecimal, + progressTracker: ProgressTracker = tracker(fixOf.name)) + : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) { + override fun filtering(elem: Any): Boolean = true + } + + private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems( + TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY)) +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index db43d527df..3c59bf41e7 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -19,10 +19,13 @@ import net.corda.core.contracts.StateRef import net.corda.core.cordapp.CordappProvider import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.CordaX500Name import net.corda.core.identity.PartyAndCertificate +import net.corda.core.messaging.DataFeed import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowProgressHandle +import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.node.* import net.corda.core.node.services.* import net.corda.core.serialization.SerializeAsToken @@ -37,6 +40,7 @@ import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.configOf import net.corda.node.services.config.parseToDbSchemaFriendlyName +import net.corda.node.services.api.* import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService @@ -148,9 +152,10 @@ open class MockServices private constructor( override val vaultService: VaultService = makeVaultService(database.hibernateConfig, schemaService) override fun recordTransactions(statesToRecord: StatesToRecord, txs: Iterable) { - super.recordTransactions(statesToRecord, txs) - // Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions. - (vaultService as VaultServiceInternal).notifyAll(statesToRecord, txs.map { it.coreTransaction }) + ServiceHubInternal.recordTransactions(statesToRecord, txs, + validatedTransactions as WritableTransactionStorage, + mockStateMachineRecordedTransactionMappingStorage, + vaultService as VaultServiceInternal) } override fun jdbcSession(): Connection = database.createSession() @@ -166,6 +171,19 @@ open class MockServices private constructor( // compiler and then the c'tor itself. return Throwable().stackTrace[3].className.split('.').dropLast(1).joinToString(".") } + + // Because Kotlin is dumb and makes not publicly visible objects public, thus changing the public API. + private val mockStateMachineRecordedTransactionMappingStorage = MockStateMachineRecordedTransactionMappingStorage() + } + + private class MockStateMachineRecordedTransactionMappingStorage : StateMachineRecordedTransactionMappingStorage { + override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) { + throw UnsupportedOperationException() + } + + override fun track(): DataFeed, StateMachineTransactionMapping> { + throw UnsupportedOperationException() + } } private constructor(cordappLoader: CordappLoader, identityService: IdentityService, networkParameters: NetworkParameters,