Fixes bugs with contract constraints (#1696)

* Added schedulable flows to cordapp scanning

Fixed a bug where the core flows are included in every cordapp. Added a test to prove the scheduled flows are loaded correctly. Added scheduled flow support to cordapp.

Renabled broken test.

Fixed test to prove cordapps aren't retreived from network.

Review fixes.

Fixed a test issue caused by gradle having slightly different paths to IntelliJ

* Fixed test for real this time.
This commit is contained in:
Clinton 2017-09-28 18:30:17 +01:00 committed by josecoll
parent 73e3690596
commit ca0090420b
19 changed files with 195 additions and 101 deletions

2
.idea/compiler.xml generated
View File

@ -41,6 +41,8 @@
<module name="explorer_test" target="1.8" />
<module name="finance_main" target="1.8" />
<module name="finance_test" target="1.8" />
<module name="gradle-plugins-cordform-common_main" target="1.8" />
<module name="gradle-plugins-cordform-common_test" target="1.8" />
<module name="graphs_main" target="1.8" />
<module name="graphs_test" target="1.8" />
<module name="irs-demo_integrationTest" target="1.8" />

View File

@ -16,6 +16,7 @@ import java.net.URL
* @property contractClassNames List of contracts
* @property initiatedFlows List of initiatable flow classes
* @property rpcFlows List of RPC initiable flows classes
* @property schedulableFlows List of flows startable by the scheduler
* @property servies List of RPC services
* @property plugins List of Corda plugin registries
* @property customSchemas List of custom schemas
@ -25,6 +26,7 @@ interface Cordapp {
val contractClassNames: List<String>
val initiatedFlows: List<Class<out FlowLogic<*>>>
val rpcFlows: List<Class<out FlowLogic<*>>>
val schedulableFlows: List<Class<out FlowLogic<*>>>
val services: List<Class<out SerializeAsToken>>
val plugins: List<CordaPluginRegistry>
val customSchemas: Set<MappedSchema>

View File

@ -11,6 +11,7 @@ data class CordappImpl(
override val contractClassNames: List<String>,
override val initiatedFlows: List<Class<out FlowLogic<*>>>,
override val rpcFlows: List<Class<out FlowLogic<*>>>,
override val schedulableFlows: List<Class<out FlowLogic<*>>>,
override val services: List<Class<out SerializeAsToken>>,
override val plugins: List<CordaPluginRegistry>,
override val customSchemas: Set<MappedSchema>,

View File

@ -114,7 +114,8 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
}
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
val contractAttachments = findAttachmentContracts(resolvedInputs, resolveContractAttachment, resolveAttachment)
val attachments = contractAttachments + (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) }).distinct()
// Order of attachments is important since contracts may refer to indexes so only append automatic attachments
val attachments = (attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } + contractAttachments).distinct()
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt)
}

View File

@ -9,6 +9,7 @@ import net.corda.core.identity.Party
* loaded or blocked.
*/
class IsolatedDummyFlow {
@StartableByRPC
@InitiatingFlow
class Initiator(val toWhom: Party) : FlowLogic<Unit>() {
@Suspendable

View File

@ -523,7 +523,7 @@ object LoggerSerializer : Serializer<Logger>() {
object ClassSerializer : Serializer<Class<*>>() {
override fun read(kryo: Kryo, input: Input, type: Class<Class<*>>): Class<*> {
val className = input.readString()
return Class.forName(className)
return Class.forName(className, true, kryo.classLoader)
}
override fun write(kryo: Kryo, output: Output, clazz: Class<*>) {

View File

@ -1,5 +1,6 @@
package net.corda.node.services
import net.corda.client.rpc.RPCException
import net.corda.core.contracts.Contract
import net.corda.core.contracts.PartyAndReference
import net.corda.core.cordapp.CordappProvider
@ -14,6 +15,7 @@ import net.corda.core.serialization.SerializationFactory
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.loggerFor
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.nodeapi.User
@ -22,6 +24,7 @@ import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.TestDependencyInjectionBase
import net.corda.testing.driver.driver
import net.corda.testing.node.MockServices
import net.corda.testing.resetTestSerialization
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
@ -38,9 +41,10 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
override val cordappProvider: CordappProvider = provider
}
companion object {
private val isolatedJAR = this::class.java.getResource("isolated.jar")!!
private val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
private companion object {
val logger = loggerFor<AttachmentLoadingTests>()
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
}
private lateinit var services: Services
@ -68,19 +72,20 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
assertEquals(expected, actual)
}
// TODO - activate this test
// @Test
@Test
fun `test that attachments retrieved over the network are not used for code`() {
driver(initialiseSerialization = false) {
val bankAName = CordaX500Name("BankA", "Zurich", "CH")
val bankBName = CordaX500Name("BankB", "Zurich", "CH")
// Copy the app jar to the first node. The second won't have it.
val path = (baseDirectory(bankAName.toString()) / "plugins").createDirectories() / "isolated.jar"
logger.info("Installing isolated jar to $path")
isolatedJAR.openStream().buffered().use { input ->
Files.newOutputStream(path).buffered().use { output ->
input.copyTo(output)
}
}
val admin = User("admin", "admin", permissions = setOf("ALL"))
val (bankA, bankB) = listOf(
startNode(providedName = bankAName, rpcUsers = listOf(admin)),
@ -96,7 +101,7 @@ class AttachmentLoadingTests : TestDependencyInjectionBase() {
val proxy = rpc.proxy
val party = proxy.wellKnownPartyFromX500Name(bankBName)!!
assertFailsWith<Exception>("xxx") {
assertFailsWith<RPCException>("net.corda.client.rpc.RPCException: net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator") {
proxy.startFlowDynamic(clazz, party).returnValue.getOrThrow()
}
}

View File

@ -146,6 +146,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
protected lateinit var database: CordaPersistence
protected var dbCloser: (() -> Any?)? = null
lateinit var cordappProvider: CordappProviderImpl
protected val cordappLoader by lazy { makeCordappLoader() }
protected val _nodeReadyFuture = openFuture<Unit>()
/** Completes once the node has successfully registered with the network map service
@ -379,7 +380,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
*/
private fun makeServices(): MutableList<Any> {
checkpointStorage = DBCheckpointStorage()
cordappProvider = CordappProviderImpl(makeCordappLoader())
cordappProvider = CordappProviderImpl(cordappLoader)
_services = ServiceHubInternalImpl()
attachments = NodeAttachmentService(services.monitoringService.metrics)
cordappProvider.start(attachments)
@ -400,10 +401,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
val scanPackages = System.getProperty("net.corda.node.cordapp.scan.packages")
return if (CordappLoader.testPackages.isNotEmpty()) {
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
CordappLoader.createWithTestPackages(CordappLoader.testPackages)
CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, CordappLoader.testPackages)
} else if (scanPackages != null) {
check(configuration.devMode) { "Package scanning can only occur in dev mode" }
CordappLoader.createWithTestPackages(scanPackages.split(","))
CordappLoader.createDefaultWithTestPackages(configuration.baseDirectory, scanPackages.split(","))
} else {
CordappLoader.createDefault(configuration.baseDirectory)
}

View File

@ -14,6 +14,7 @@ import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.utilities.*
import net.corda.node.VersionInfo
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.node.serialization.KryoServerSerializationScheme
import net.corda.node.serialization.NodeClock
import net.corda.node.services.RPCUserService
@ -340,14 +341,15 @@ open class Node(override val configuration: FullNodeConfiguration,
}
private fun initialiseSerialization() {
val classloader = cordappLoader.appClassLoader
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
registerScheme(KryoServerSerializationScheme())
registerScheme(AMQPServerSerializationScheme())
}
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT
SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT
SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT.withClassLoader(classloader)
SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT.withClassLoader(classloader)
SerializationDefaults.STORAGE_CONTEXT = KRYO_STORAGE_CONTEXT.withClassLoader(classloader)
SerializationDefaults.CHECKPOINT_CONTEXT = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader)
}
/** Starts a blocking event loop for message dispatch. */

View File

@ -5,10 +5,7 @@ import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.contracts.Contract
import net.corda.core.contracts.UpgradedContract
import net.corda.core.cordapp.Cordapp
import net.corda.core.flows.ContractUpgradeFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.StartableByRPC
import net.corda.core.flows.*
import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.node.CordaPluginRegistry
@ -40,10 +37,17 @@ import kotlin.streams.toList
* @property cordappJarPaths The classpath of cordapp JARs
*/
class CordappLoader private constructor(private val cordappJarPaths: List<URL>) {
val cordapps: List<Cordapp> by lazy { loadCordapps() }
val cordapps: List<Cordapp> by lazy { loadCordapps() + coreCordapp }
@VisibleForTesting
internal val appClassLoader: ClassLoader = javaClass.classLoader
internal val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.toTypedArray(), javaClass.classLoader)
init {
if (cordappJarPaths.isEmpty()) {
logger.info("No CorDapp paths provided")
} else {
logger.info("Loading CorDapps from ${cordappJarPaths.joinToString()}")
}
}
companion object {
private val logger = loggerFor<CordappLoader>()
@ -54,19 +58,31 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
* @param baseDir The directory that this node is running in. Will use this to resolve the plugins directory
* for classpath scanning.
*/
fun createDefault(baseDir: Path): CordappLoader {
val pluginsDir = getPluginsPath(baseDir)
return CordappLoader(if (!pluginsDir.exists()) emptyList<URL>() else pluginsDir.list {
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
})
}
fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins"
fun createDefault(baseDir: Path) = CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir)))
/**
* Create a dev mode CordappLoader for test environments
* Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath
* and plugins directory. This is intended mostly for use by the driver.
*
* @param baseDir See [createDefault.baseDir]
* @param testPackages See [createWithTestPackages.testPackages]
*/
fun createWithTestPackages(testPackages: List<String> = CordappLoader.testPackages) = CordappLoader(testPackages.flatMap(this::createScanPackage))
@VisibleForTesting
@JvmOverloads
fun createDefaultWithTestPackages(baseDir: Path, testPackages: List<String> = CordappLoader.testPackages)
= CordappLoader(getCordappsInDirectory(getPluginsPath(baseDir)) + testPackages.flatMap(this::createScanPackage))
/**
* Create a dev mode CordappLoader for test environments that creates and loads cordapps from the classpath.
* This is intended for use in unit and integration tests.
*
* @param testPackages List of package names that contain CorDapp classes that can be automatically turned into
* CorDapps.
*/
@VisibleForTesting
@JvmOverloads
fun createWithTestPackages(testPackages: List<String> = CordappLoader.testPackages)
= CordappLoader(testPackages.flatMap(this::createScanPackage))
/**
* Creates a dev mode CordappLoader intended only to be used in test environments
@ -76,6 +92,8 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
@VisibleForTesting
fun createDevMode(scanJars: List<URL>) = CordappLoader(scanJars)
private fun getPluginsPath(baseDir: Path): Path = baseDir / "plugins"
private fun createScanPackage(scanPackage: String): List<URL> {
val resource = scanPackage.replace('.', '/')
return this::class.java.classLoader.getResources(resource)
@ -90,7 +108,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
.toList()
}
/** Takes a package of classes and creates a JAR from them - only use in tests */
/** Takes a package of classes and creates a JAR from them - only use in tests. */
private fun createDevCordappJar(scanPackage: String, path: URL, jarPackageName: String): URI {
if(!generatedCordapps.contains(path)) {
val cordappDir = File("build/tmp/generated-test-cordapps")
@ -118,12 +136,41 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
return generatedCordapps[path]!!
}
private fun getCordappsInDirectory(pluginsDir: Path): List<URL> {
return if (!pluginsDir.exists()) {
emptyList<URL>()
} else {
pluginsDir.list {
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
}
}
}
/**
* A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only
* A list of test packages that will be scanned as CorDapps and compiled into CorDapp JARs for use in tests only.
*/
@VisibleForTesting
var testPackages: List<String> = emptyList()
private val generatedCordapps = mutableMapOf<URL, URI>()
/** A list of the core RPC flows present in Corda */
private val coreRPCFlows = listOf(
ContractUpgradeFlow.Initiate::class.java,
ContractUpgradeFlow.Authorise::class.java,
ContractUpgradeFlow.Deauthorise::class.java)
/** A Cordapp representing the core package which is not scanned automatically. */
@VisibleForTesting
internal val coreCordapp = CordappImpl(
listOf(),
listOf(),
coreRPCFlows,
listOf(),
listOf(),
listOf(),
setOf(),
ContractUpgradeFlow.javaClass.protectionDomain.codeSource.location // Core JAR location
)
}
private fun loadCordapps(): List<Cordapp> {
@ -132,6 +179,7 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
CordappImpl(findContractClassNames(scanResult),
findInitiatedFlows(scanResult),
findRPCFlows(scanResult),
findSchedulableFlows(scanResult),
findServices(scanResult),
findPlugins(it),
findCustomSchemas(scanResult),
@ -163,13 +211,11 @@ class CordappLoader private constructor(private val cordappJarPaths: List<URL>)
return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers))
}
val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
val coreFlows = listOf(
ContractUpgradeFlow.Initiate::class.java,
ContractUpgradeFlow.Authorise::class.java,
ContractUpgradeFlow.Deauthorise::class.java
)
return found + coreFlows
return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
}
private fun findSchedulableFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesWithAnnotation(FlowLogic::class, SchedulableFlow::class)
}
private fun findContractClassNames(scanResult: ScanResult): List<String> {

View File

@ -66,7 +66,7 @@ open class CordappProviderImpl(private val cordappLoader: CordappLoader) : Singl
* @return A cordapp context for the given CorDapp
*/
fun getAppContext(cordapp: Cordapp): CordappContext {
return CordappContext(cordapp, getCordappAttachmentId(cordapp), URLClassLoader(arrayOf(cordapp.jarPath), cordappLoader.appClassLoader))
return CordappContext(cordapp, getCordappAttachmentId(cordapp), cordappLoader.appClassLoader)
}
/**

View File

@ -1,54 +0,0 @@
package net.corda.node.cordapp
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.node.internal.cordapp.CordappLoader
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.nio.file.Paths
@InitiatingFlow
class DummyFlow : FlowLogic<Unit>() {
override fun call() { }
}
@InitiatedBy(DummyFlow::class)
class LoaderTestFlow : FlowLogic<Unit>() {
override fun call() { }
}
class CordappLoaderTest {
@Test
fun `test that classes that aren't in cordapps aren't loaded`() {
// Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp
val loader = CordappLoader.createDefault(Paths.get("."))
assertThat(loader.cordapps).isEmpty()
}
@Test
fun `test that classes that are in a cordapp are loaded`() {
val loader = CordappLoader.createWithTestPackages(listOf("net.corda.node.cordapp"))
val initiatedFlows = loader.cordapps.first().initiatedFlows
val expectedClass = loader.appClassLoader.loadClass("net.corda.node.cordapp.LoaderTestFlow").asSubclass(FlowLogic::class.java)
assertThat(initiatedFlows).contains(expectedClass)
}
@Test
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val actual = loader.cordapps.toTypedArray()
assertThat(actual).hasSize(1)
val actualCordapp = actual.first()
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf("net.corda.finance.contracts.isolated.AnotherDummyContract"))
assertThat(actualCordapp.initiatedFlows).isEmpty()
assertThat(actualCordapp.rpcFlows).contains(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiate").asSubclass(FlowLogic::class.java))
assertThat(actualCordapp.services).isEmpty()
assertThat(actualCordapp.plugins).hasSize(1)
assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin")
assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR)
}
}

View File

@ -0,0 +1,89 @@
package net.corda.node.internal.cordapp
import net.corda.core.flows.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.nio.file.Paths
@InitiatingFlow
class DummyFlow : FlowLogic<Unit>() {
override fun call() { }
}
@InitiatedBy(DummyFlow::class)
class LoaderTestFlow(unusedSession: FlowSession) : FlowLogic<Unit>() {
override fun call() { }
}
@SchedulableFlow
class DummySchedulableFlow : FlowLogic<Unit>() {
override fun call() { }
}
@StartableByRPC
class DummyRPCFlow : FlowLogic<Unit>() {
override fun call() { }
}
class CordappLoaderTest {
private companion object {
val testScanPackages = listOf("net.corda.node.internal.cordapp")
val isolatedContractId = "net.corda.finance.contracts.isolated.AnotherDummyContract"
val isolatedFlowName = "net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Initiator"
}
@Test
fun `test that classes that aren't in cordapps aren't loaded`() {
// Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp
val loader = CordappLoader.createDefault(Paths.get("."))
assertThat(loader.cordapps)
.hasSize(1)
.contains(CordappLoader.coreCordapp)
}
@Test
fun `isolated JAR contains a CorDapp with a contract and plugin`() {
val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val actual = loader.cordapps.toTypedArray()
assertThat(actual).hasSize(2)
val actualCordapp = actual.single { it != CordappLoader.coreCordapp }
assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId))
assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor")
assertThat(actualCordapp.rpcFlows).isEmpty()
assertThat(actualCordapp.schedulableFlows).isEmpty()
assertThat(actualCordapp.services).isEmpty()
assertThat(actualCordapp.plugins).hasSize(1)
assertThat(actualCordapp.plugins.first().javaClass.name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedPlugin")
assertThat(actualCordapp.jarPath).isEqualTo(isolatedJAR)
}
@Test
fun `flows are loaded by loader`() {
val loader = CordappLoader.createWithTestPackages(testScanPackages)
val actual = loader.cordapps.toTypedArray()
// One core cordapp, one cordapp from this source tree, and two others due to identically named locations
// in resources and the non-test part of node. This is okay due to this being test code. In production this
// cannot happen. In gradle it will also pick up the node jar.
assertThat(actual.size == 4 || actual.size == 5).isTrue()
val actualCordapp = actual.single { !it.initiatedFlows.isEmpty() }
assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java)
assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java)
assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java)
}
// This test exists because the appClassLoader is used by serialisation and we need to ensure it is the classloader
// being used internally. Later iterations will use a classloader per cordapp and this test can be retired.
@Test
fun `cordapp classloader can load cordapp classes`() {
val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
loader.appClassLoader.loadClass(isolatedContractId)
loader.appClassLoader.loadClass(isolatedFlowName)
}
}

View File

@ -1,8 +1,6 @@
package net.corda.node.cordapp
package net.corda.node.internal.cordapp
import net.corda.core.node.services.AttachmentStorage
import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.cordapp.CordappProviderImpl
import net.corda.testing.node.MockAttachmentStorage
import org.junit.Assert
import org.junit.Before

View File

@ -14,7 +14,7 @@ class MockCordappProvider(cordappLoader: CordappLoader) : CordappProviderImpl(co
val cordappRegistry = mutableListOf<Pair<Cordapp, AttachmentId>>()
fun addMockCordapp(contractClassName: ContractClassName, services: ServiceHub) {
val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL())
val cordapp = CordappImpl(listOf(contractClassName), emptyList(), emptyList(), emptyList(), emptyList(), emptyList(), emptySet(), Paths.get(".").toUri().toURL())
if (cordappRegistry.none { it.first.contractClassNames.contains(contractClassName) }) {
cordappRegistry.add(Pair(cordapp, findOrImportAttachment(contractClassName.toByteArray(), services)))
}