From 139eef2ccb6459b6ab0f5130216a85615657dcf5 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 8 Aug 2017 15:35:55 +0100 Subject: [PATCH 001/101] Move AMQP serializer toward the new common abstract itnerface Allow unique serializer factories to be created per unique set of WhiteList / Class Loader pair. Remove default consruction of the SerializerFactory to force use of the FactoryFactory to get the generic factory and thus access to it's ClassLoader --- .../serialization/AMQPSerializationScheme.kt | 8 +- .../serialization/KryoAMQPSerializer.kt | 3 +- .../serialization/amqp/CustomSerializer.kt | 6 +- .../amqp/DeserializationInput.kt | 2 +- .../serialization/amqp/SerializationOutput.kt | 2 +- .../serialization/amqp/SerializerFactory.kt | 31 +++--- .../amqp/SerializerFactoryFactory.kt | 22 +++++ .../carpenter/AMQPSchemaExtensions.kt | 61 ++++-------- .../serialization/carpenter/ClassCarpenter.kt | 7 +- .../serialization/carpenter/MetaCarpenter.kt | 4 +- .../serialization/amqp/AMQPTestUtils.kt | 2 +- .../amqp/DeserializeAndReturnEnvelopeTests.kt | 4 +- ...erializeNeedingCarpentrySimpleTypesTest.kt | 57 ++++++----- .../amqp/DeserializeNeedingCarpentryTests.kt | 70 ++++++-------- .../amqp/JavaSerializationOutputTests.java | 3 +- .../amqp/SerializationOutputTests.kt | 94 +++++++++---------- .../carpenter/ClassCarpenterTestUtils.kt | 4 +- ...berCompositeSchemaToClassCarpenterTests.kt | 14 +-- .../InheritanceSchemaToClassCarpenterTests.kt | 26 ++--- ...berCompositeSchemaToClassCarpenterTests.kt | 10 +- ...berCompositeSchemaToClassCarpenterTests.kt | 30 ++++-- 21 files changed, 241 insertions(+), 219 deletions(-) create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index 3d1ca75f34..5e5186c343 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -41,11 +41,17 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { rpcClientSerializerFactory(context) SerializationContext.UseCase.RPCServer -> rpcServerSerializerFactory(context) - else -> SerializerFactory(context.whitelist) // TODO pass class loader also + else -> SerializerFactory(context.whitelist, context.deserializationClassLoader) } }.also { registerCustomSerializers(it) } } + fun getSerializerFactory(): SerializerFactory { + return serializerFactoriesForContexts.computeIfAbsent(Pair(AllWhitelist, deserializationClassLoader)) { + SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + } + } + override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { val serializerFactory = getSerializerFactory(context) return DeserializationInput(serializerFactory).deserialize(byteSequence, clazz) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt index 16dec8a83e..dae649b285 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt @@ -12,6 +12,7 @@ import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactoryFactory /** * This [Kryo] custom [Serializer] switches the object graph of anything annotated with `@CordaSerializable` @@ -34,4 +35,4 @@ class KryoAMQPSerializer(val serializationFactory: SerializationFactory, val ser input.readBytes(allBytes, peekedBytes.size, size - peekedBytes.size) return serializationFactory.deserialize(allBytes.sequence(), type, serializationContext) } -} \ No newline at end of file +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 58752b0ecc..51911f2d24 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -151,8 +151,10 @@ abstract class CustomSerializer : AMQPSerializer { * @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ abstract class ToString(clazz: Class, withInheritance: Boolean = false, - private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> { string -> `constructor`.newInstance(string) } }, - private val unmaker: (T) -> String = { obj -> obj.toString() }) : Proxy(clazz, String::class.java, /* Unused */ SerializerFactory(), withInheritance) { + private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { + `constructor` -> { string -> `constructor`.newInstance(string) } + }, + private val unmaker: (T) -> String = { obj -> obj.toString() }) : Proxy(clazz, String::class.java, /* Unused */ SerializerFactoryFactory.get(), withInheritance) { override val additionalSerializers: Iterable> = emptyList() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 5654dfa20d..8764064116 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -20,7 +20,7 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -class DeserializationInput(internal val serializerFactory: SerializerFactory = SerializerFactory()) { +class DeserializationInput(internal val serializerFactory: SerializerFactory = SerializerFactoryFactory.get()) { // TODO: we're not supporting object refs yet private val objectHistory: MutableList = ArrayList() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index 90011f149c..7e47932f06 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -14,7 +14,7 @@ import kotlin.collections.LinkedHashSet * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -open class SerializationOutput(internal val serializerFactory: SerializerFactory = SerializerFactory()) { +open class SerializationOutput(internal val serializerFactory: SerializerFactory = SerializerFactoryFactory.get()) { // TODO: we're not supporting object refs yet private val objectHistory: MutableMap = IdentityHashMap() private val serializerHistory: MutableSet> = LinkedHashSet() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 9b20b08a5c..b88c89d99c 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -44,11 +44,13 @@ import javax.annotation.concurrent.ThreadSafe // TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc. // TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact? @ThreadSafe -class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { +class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() private val customSerializers = CopyOnWriteArrayList>() - private val classCarpenter = ClassCarpenter() + private val classCarpenter = ClassCarpenter(cl) + val classloader : ClassLoader + get() = classCarpenter.classloader /** * Look up, and manufacture if necessary, a serializer for the given type. @@ -176,15 +178,18 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } } + /** + * Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd + * if not use the [ClassCarpenter] to generate a class to use in it's place + */ private fun processSchema(schema: Schema, sentinal: Boolean = false) { val carpenterSchemas = CarpenterSchemas.newInstance() for (typeNotation in schema.types) { try { - processSchemaEntry(typeNotation, classCarpenter.classloader) - } catch (e: ClassNotFoundException) { + } + catch (e: ClassNotFoundException) { if (sentinal || (typeNotation !is CompositeType)) throw e - typeNotation.carpenterSchema( - classLoaders = listOf(classCarpenter.classloader), carpenterSchemas = carpenterSchemas) + typeNotation.carpenterSchema(classloader, carpenterSchemas = carpenterSchemas) } } @@ -195,10 +200,9 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } } - private fun processSchemaEntry(typeNotation: TypeNotation, - cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader) { + private fun processSchemaEntry(typeNotation: TypeNotation) { when (typeNotation) { - is CompositeType -> processCompositeType(typeNotation, cl) // java.lang.Class (whether a class or interface) + is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface) is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics } } @@ -210,7 +214,6 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } private fun processCompositeType(typeNotation: CompositeType, - cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader) { // TODO: class loader logic, and compare the schema. val type = typeForName(typeNotation.name, cl) get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type) @@ -322,11 +325,9 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { else -> throw NotSerializableException("Unable to render type $type to a string.") } - private fun typeForName( - name: String, - cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader): Type { + private fun typeForName(name: String, classloader: ClassLoader): Type { return if (name.endsWith("[]")) { - val elementType = typeForName(name.substring(0, name.lastIndex - 1)) + val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader) if (elementType is ParameterizedType || elementType is GenericArrayType) { DeserializedGenericArrayType(elementType) } else if (elementType is Class<*>) { @@ -349,7 +350,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { else -> throw NotSerializableException("Not able to deserialize array type: $name") } } else { - DeserializedParameterizedType.make(name, cl) + DeserializedParameterizedType.make(name, classloader) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt new file mode 100644 index 0000000000..81a2643918 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt @@ -0,0 +1,22 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.ClassWhitelist +import net.corda.nodeapi.internal.serialization.AllWhitelist + +/** + * Factory singleton that maps unique Serializer Factories from a pair of WhitleList and ClassLoader + */ +object SerializerFactoryFactory { + val factories : MutableMap, SerializerFactory> = mutableMapOf() + + fun get(context: SerializationContext) : SerializerFactory = + factories.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { + SerializerFactory(context.whitelist, context.deserializationClassLoader) + } + + fun get() : SerializerFactory = + factories.computeIfAbsent(Pair(AllWhitelist, ClassLoader.getSystemClassLoader())) { + SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt index dbe8bb254d..eb1f21f54a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt @@ -4,13 +4,11 @@ import net.corda.nodeapi.internal.serialization.amqp.CompositeType import net.corda.nodeapi.internal.serialization.amqp.Field as AMQPField import net.corda.nodeapi.internal.serialization.amqp.Schema as AMQPSchema -fun AMQPSchema.carpenterSchema( - loaders: List = listOf(ClassLoader.getSystemClassLoader())) - : CarpenterSchemas { +fun AMQPSchema.carpenterSchema(classloader: ClassLoader) : CarpenterSchemas { val rtn = CarpenterSchemas.newInstance() types.filterIsInstance().forEach { - it.carpenterSchema(classLoaders = loaders, carpenterSchemas = rtn) + it.carpenterSchema(classloader, carpenterSchemas = rtn) } return rtn @@ -19,10 +17,9 @@ fun AMQPSchema.carpenterSchema( /** * if we can load the class then we MUST know about all of it's composite elements */ -private fun CompositeType.validatePropertyTypes( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader())) { +private fun CompositeType.validatePropertyTypes(classloader: ClassLoader) { fields.forEach { - if (!it.validateType(classLoaders)) throw UncarpentableException(name, it.name, it.type) + if (!it.validateType(classloader)) throw UncarpentableException(name, it.name, it.type) } } @@ -34,24 +31,21 @@ fun AMQPField.typeAsString() = if (type == "*") requires[0] else type * b) add the class to the dependency tree in [carpenterSchemas] if it cannot be instantiated * at this time * - * @param classLoaders list of classLoaders, defaulting toe the system class loader, that might - * be used to load objects + * @param classloader the class loader provided dby the [SerializationContext] * @param carpenterSchemas structure that holds the dependency tree and list of classes that * need constructing * @param force by default a schema is not added to [carpenterSchemas] if it already exists * on the class path. For testing purposes schema generation can be forced */ -fun CompositeType.carpenterSchema( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader()), - carpenterSchemas: CarpenterSchemas, - force: Boolean = false) { - if (classLoaders.exists(name)) { - validatePropertyTypes(classLoaders) +fun CompositeType.carpenterSchema(classloader: ClassLoader, + carpenterSchemas: CarpenterSchemas, + force: Boolean = false) { + if (classloader.exists(name)) { + validatePropertyTypes(classloader) if (!force) return } val providesList = mutableListOf>() - var isInterface = false var isCreatable = true @@ -62,7 +56,7 @@ fun CompositeType.carpenterSchema( } try { - providesList.add(classLoaders.loadIfExists(it)) + providesList.add(classloader.loadClass(it)) } catch (e: ClassNotFoundException) { carpenterSchemas.addDepPair(this, name, it) isCreatable = false @@ -73,7 +67,7 @@ fun CompositeType.carpenterSchema( fields.forEach { try { - m[it.name] = FieldFactory.newInstance(it.mandatory, it.name, it.getTypeAsClass(classLoaders)) + m[it.name] = FieldFactory.newInstance(it.mandatory, it.name, it.getTypeAsClass(classloader)) } catch (e: ClassNotFoundException) { carpenterSchemas.addDepPair(this, name, it.typeAsString()) isCreatable = false @@ -110,33 +104,18 @@ val typeStrToType: Map, Class> = mapOf( Pair("byte", false) to Byte::class.javaObjectType ) -fun AMQPField.getTypeAsClass( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader()) -) = typeStrToType[Pair(type, mandatory)] ?: when (type) { +fun AMQPField.getTypeAsClass(classloader: ClassLoader) = typeStrToType[Pair(type, mandatory)] ?: when (type) { "string" -> String::class.java - "*" -> classLoaders.loadIfExists(requires[0]) - else -> classLoaders.loadIfExists(type) + "*" -> classloader.loadClass(requires[0]) + else -> classloader.loadClass(type) } -fun AMQPField.validateType( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader()) -) = when (type) { +fun AMQPField.validateType(classloader: ClassLoader) = when (type) { "byte", "int", "string", "short", "long", "char", "boolean", "double", "float" -> true - "*" -> classLoaders.exists(requires[0]) - else -> classLoaders.exists(type) + "*" -> classloader.exists(requires[0]) + else -> classloader.exists(type) } -private fun List.exists(clazz: String) = this.find { - try { it.loadClass(clazz); true } catch (e: ClassNotFoundException) { false } -} != null +private fun ClassLoader.exists(clazz: String) = run { + try { this.loadClass(clazz); true } catch (e: ClassNotFoundException) { false } } -private fun List.loadIfExists(clazz: String): Class<*> { - this.forEach { - try { - return it.loadClass(clazz) - } catch (e: ClassNotFoundException) { - return@forEach - } - } - throw ClassNotFoundException(clazz) -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt index 21641a0ae1..a62be2da34 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt @@ -18,7 +18,8 @@ interface SimpleFieldAccess { operator fun get(name: String): Any? } -class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoader) { +class CarpenterClassLoader (parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) : + ClassLoader(parentClassLoader) { fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size) } @@ -66,14 +67,14 @@ class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoad * * Equals/hashCode methods are not yet supported. */ -class ClassCarpenter { +class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader) { // TODO: Generics. // TODO: Sandbox the generated code when a security manager is in use. // TODO: Generate equals/hashCode. // TODO: Support annotations. // TODO: isFoo getter patterns for booleans (this is what Kotlin generates) - val classloader = CarpenterClassLoader() + val classloader = CarpenterClassLoader(cl) private val _loaded = HashMap>() private val String.jvm: String get() = replace(".", "/") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt index 77e866fb32..fb0d1f7001 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt @@ -73,9 +73,7 @@ abstract class MetaCarpenterBase (val schemas : CarpenterSchemas, val cc : Class // we're out of blockers so we can now create the type if (schemas.dependencies[dependent]?.second?.isEmpty() ?: false) { (schemas.dependencies.remove (dependent)?.first as CompositeType).carpenterSchema ( - classLoaders = listOf ( - ClassLoader.getSystemClassLoader(), - cc.classloader), + classloader = cc.classloader, carpenterSchemas = schemas) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt index 19b93b6542..c22a70f4d9 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt @@ -4,7 +4,7 @@ import org.apache.qpid.proton.codec.Data class TestSerializationOutput( private val verbose: Boolean, - serializerFactory: SerializerFactory = SerializerFactory()) : SerializationOutput(serializerFactory) { + serializerFactory: SerializerFactory = SerializerFactoryFactory.get()) : SerializationOutput(serializerFactory) { override fun writeSchema(schema: Schema, data: Data) { if (verbose) println(schema) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt index b91fa3fe9e..39ed38bea4 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt @@ -18,7 +18,7 @@ class DeserializeAndReturnEnvelopeTests { val a = A(10, "20") - val factory = SerializerFactory() + val factory = SerializerFactoryFactory.get() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) @@ -34,7 +34,7 @@ class DeserializeAndReturnEnvelopeTests { val b = B(A(10, "20"), 30.0F) - val factory = SerializerFactory() + val factory = SerializerFactoryFactory.get() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt index 76933fb370..01fb68a22f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt @@ -8,7 +8,7 @@ import net.corda.nodeapi.internal.serialization.carpenter.* // those classes don't exist within the system's Class Loader the deserialiser will be forced to carpent // versions of them up using its own internal class carpenter (each carpenter houses it's own loader). This // replicates the situation where a receiver doesn't have some or all elements of a schema present on it's classpath -class DeserializeNeedingCarpentrySimpleTypesTest { +class DeserializeNeedingCarpentrySimpleTypesTest : AmqpCarpenterBase() { companion object { /** * If you want to see the schema encoded into the envelope after serialisation change this to true @@ -21,7 +21,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleInt() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "int" to NonNullableField(Integer::class.javaPrimitiveType!!) ))) @@ -41,7 +41,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleIntNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "int" to NullableField(Integer::class.java) ))) @@ -57,7 +57,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleIntNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "int" to NullableField(Integer::class.java) ))) @@ -73,7 +73,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleChar() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "char" to NonNullableField(Character::class.javaPrimitiveType!!) ))) @@ -86,7 +86,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleCharNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "char" to NullableField(Character::class.javaObjectType) ))) @@ -99,7 +99,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleCharNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "char" to NullableField(java.lang.Character::class.java) ))) @@ -112,7 +112,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleLong() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "long" to NonNullableField(Long::class.javaPrimitiveType!!) ))) @@ -126,7 +126,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleLongNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "long" to NullableField(Long::class.javaObjectType) ))) @@ -140,7 +140,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleLongNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "long" to NullableField(Long::class.javaObjectType) ))) @@ -153,7 +153,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleBoolean() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "boolean" to NonNullableField(Boolean::class.javaPrimitiveType!!) ))) @@ -166,7 +166,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleBooleanNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "boolean" to NullableField(Boolean::class.javaObjectType) ))) @@ -179,7 +179,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleBooleanNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "boolean" to NullableField(Boolean::class.javaObjectType) ))) @@ -192,7 +192,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleDouble() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "double" to NonNullableField(Double::class.javaPrimitiveType!!) ))) @@ -205,7 +205,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleDoubleNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "double" to NullableField(Double::class.javaObjectType) ))) @@ -218,7 +218,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleDoubleNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "double" to NullableField(Double::class.javaObjectType) ))) @@ -231,7 +231,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleShort() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "short" to NonNullableField(Short::class.javaPrimitiveType!!) ))) @@ -244,7 +244,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleShortNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "short" to NullableField(Short::class.javaObjectType) ))) @@ -257,7 +257,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleShortNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "short" to NullableField(Short::class.javaObjectType) ))) @@ -270,7 +270,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleFloat() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "float" to NonNullableField(Float::class.javaPrimitiveType!!) ))) @@ -283,7 +283,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleFloatNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "float" to NullableField(Float::class.javaObjectType) ))) @@ -296,7 +296,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleFloatNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "float" to NullableField(Float::class.javaObjectType) ))) @@ -309,7 +309,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleByte() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "byte" to NonNullableField(Byte::class.javaPrimitiveType!!) ))) @@ -324,7 +324,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleByteNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "byte" to NullableField(Byte::class.javaObjectType) ))) @@ -339,7 +339,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleByteNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "byte" to NullableField(Byte::class.javaObjectType) ))) @@ -353,11 +353,10 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun simpleTypeKnownInterface() { val clazz = ClassCarpenter().build (ClassSchema( - "oneType", mapOf("name" to NonNullableField(String::class.java)), + testName(), mapOf("name" to NonNullableField(String::class.java)), interfaces = listOf (I::class.java))) val testVal = "Some Person" val classInstance = clazz.constructors[0].newInstance(testVal) - val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(classInstance) val deserializedObj = DeserializationInput(sf2).deserialize(serialisedBytes) @@ -368,7 +367,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun manyTypes() { - val manyClass = ClassCarpenter().build (ClassSchema("many", mapOf( + val manyClass = ClassCarpenter().build (ClassSchema(testName(), mapOf( "intA" to NonNullableField (Int::class.java), "intB" to NullableField (Integer::class.java), "intC" to NullableField (Integer::class.java), @@ -397,7 +396,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { "byteB" to NullableField (Byte::class.javaObjectType), "byteC" to NullableField (Byte::class.javaObjectType)))) - val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize( + val serialisedBytes = TestSerializationOutput(VERBOSE, factory).serialize( manyClass.constructors.first().newInstance( 1, 2, null, "a", "b", null, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt index d0cae9e3b6..d8feeb8473 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt @@ -3,22 +3,21 @@ package net.corda.nodeapi.internal.serialization.amqp import org.junit.Test import kotlin.test.* import net.corda.nodeapi.internal.serialization.carpenter.* +import net.corda.nodeapi.internal.serialization.AllWhitelist interface I { fun getName() : String } -/** - * These tests work by having the class carpenter build the classes we serialise and then deserialise them - * within the context of a second serialiser factory. The second factory is required as the first, having - * been used to serialise the class, will have cached a copy of the class and will thus bypass the need - * to pull it out of the class loader. - * - * However, those classes don't exist within the system's Class Loader and thus the deserialiser will be forced - * to carpent versions of them up using its own internal class carpenter (each carpenter houses it's own loader). This - * replicates the situation where a receiver doesn't have some or all elements of a schema present on it's classpath - */ -class DeserializeNeedingCarpentryTests { +// These tests work by having the class carpenter build the classes we serialise and then deserialise them +// within the context of a second serialiser factory. The second factory is required as the first, having +// been used to serialise the class, will have cached a copy of the class and will thus bypass the need +// to pull it out of the class loader. +// +// However, those classes don't exist within the system's Class Loader and thus the deserialiser will be forced +// to carpent versions of them up using its own internal class carpenter (each carpenter houses it's own loader). This +// replicates the situation where a receiver doesn't have some or all elements of a schema present on it's classpath +class DeserializeNeedingCarpentryTests : AmqpCarpenterBase() { companion object { /** * If you want to see the schema encoded into the envelope after serialisation change this to true @@ -32,7 +31,7 @@ class DeserializeNeedingCarpentryTests { @Test fun verySimpleType() { val testVal = 10 - val clazz = ClassCarpenter().build(ClassSchema("oneType", mapOf("a" to NonNullableField(Int::class.java)))) + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf("a" to NonNullableField(Int::class.java)))) val classInstance = clazz.constructors[0].newInstance(testVal) val serialisedBytes = TestSerializationOutput(VERBOSE, sf1).serialize(classInstance) @@ -66,16 +65,20 @@ class DeserializeNeedingCarpentryTests { val testValA = 10 val testValB = 20 val testValC = 20 - val clazz = ClassCarpenter().build(ClassSchema("oneType", mapOf("a" to NonNullableField(Int::class.java)))) + val clazz = ClassCarpenter().build(ClassSchema("${testName()}_clazz", + mapOf("a" to NonNullableField(Int::class.java)))) + val concreteA = clazz.constructors[0].newInstance(testValA) val concreteB = clazz.constructors[0].newInstance(testValB) val concreteC = clazz.constructors[0].newInstance(testValC) - val deserialisedA = DeserializationInput(sf2).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(concreteA)) + val deserialisedA = DeserializationInput(sf2).deserialize( + TestSerializationOutput(VERBOSE, sf1).serialize(concreteA)) assertEquals (testValA, deserialisedA::class.java.getMethod("getA").invoke(deserialisedA)) - val deserialisedB = DeserializationInput(sf2).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(concreteB)) + val deserialisedB = DeserializationInput(sf2).deserialize( + TestSerializationOutput(VERBOSE, sf1).serialize(concreteB)) assertEquals (testValB, deserialisedA::class.java.getMethod("getA").invoke(deserialisedB)) assertEquals (deserialisedA::class.java, deserialisedB::class.java) @@ -84,8 +87,10 @@ class DeserializeNeedingCarpentryTests { // won't already exist and it will be carpented a second time showing that when A and B are the // same underlying class that we didn't create a second instance of the class with the // second deserialisation - val lsf = SerializerFactory() - val deserialisedC = DeserializationInput(lsf).deserialize(TestSerializationOutput(VERBOSE, lsf).serialize(concreteC)) + val lfactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val deserialisedC = DeserializationInput(lfactory).deserialize( + TestSerializationOutput(VERBOSE, lfactory).serialize(concreteC)) + assertEquals (testValC, deserialisedC::class.java.getMethod("getA").invoke(deserialisedC)) assertNotEquals (deserialisedA::class.java, deserialisedC::class.java) assertNotEquals (deserialisedB::class.java, deserialisedC::class.java) @@ -94,7 +99,7 @@ class DeserializeNeedingCarpentryTests { @Test fun simpleTypeKnownInterface() { val clazz = ClassCarpenter().build (ClassSchema( - "oneType", mapOf("name" to NonNullableField(String::class.java)), + testName(), mapOf("name" to NonNullableField(String::class.java)), interfaces = listOf (I::class.java))) val testVal = "Some Person" val classInstance = clazz.constructors[0].newInstance(testVal) @@ -108,7 +113,7 @@ class DeserializeNeedingCarpentryTests { @Test fun arrayOfTypes() { - val clazz = ClassCarpenter().build(ClassSchema("oneType", mapOf("a" to NonNullableField(Int::class.java)))) + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf("a" to NonNullableField(Int::class.java)))) data class Outer (val a : Array) @@ -142,8 +147,8 @@ class DeserializeNeedingCarpentryTests { fun reusedClasses() { val cc = ClassCarpenter() - val innerType = cc.build(ClassSchema("inner", mapOf("a" to NonNullableField(Int::class.java)))) - val outerType = cc.build(ClassSchema("outer", mapOf("a" to NonNullableField(innerType)))) + val innerType = cc.build(ClassSchema("${testName()}.inner", mapOf("a" to NonNullableField(Int::class.java)))) + val outerType = cc.build(ClassSchema("${testName()}.outer", mapOf("a" to NonNullableField(innerType)))) val inner = innerType.constructors[0].newInstance(1) val outer = outerType.constructors[0].newInstance(innerType.constructors[0].newInstance(2)) @@ -160,7 +165,7 @@ class DeserializeNeedingCarpentryTests { @Test fun nestedTypes() { val cc = ClassCarpenter() - val nestedClass = cc.build (ClassSchema("nestedType", + val nestedClass = cc.build (ClassSchema(testName(), mapOf("name" to NonNullableField(String::class.java)))) val outerClass = cc.build (ClassSchema("outerType", @@ -169,23 +174,6 @@ class DeserializeNeedingCarpentryTests { val classInstance = outerClass.constructors.first().newInstance(nestedClass.constructors.first().newInstance("name")) val serialisedBytes = TestSerializationOutput(VERBOSE, sf1).serialize(classInstance) val deserializedObj = DeserializationInput(sf2).deserialize(serialisedBytes) - - val inner = deserializedObj::class.java.getMethod("getInner").invoke(deserializedObj) - assertEquals("name", inner::class.java.getMethod("getName").invoke(inner)) - } - - @Test - fun repeatedNestedTypes() { - val cc = ClassCarpenter() - val nestedClass = cc.build (ClassSchema("nestedType", - mapOf("name" to NonNullableField(String::class.java)))) - - data class outer(val a: Any, val b: Any) - - val classInstance = outer ( - nestedClass.constructors.first().newInstance("foo"), - nestedClass.constructors.first().newInstance("bar")) - val serialisedBytes = TestSerializationOutput(VERBOSE, sf1).serialize(classInstance) val deserializedObj = DeserializationInput(sf2).deserialize(serialisedBytes) @@ -195,7 +183,7 @@ class DeserializeNeedingCarpentryTests { @Test fun listOfType() { - val unknownClass = ClassCarpenter().build (ClassSchema("unknownClass", mapOf( + val unknownClass = ClassCarpenter().build (ClassSchema(testName(), mapOf( "v1" to NonNullableField(Int::class.java), "v2" to NonNullableField(Int::class.java)))) @@ -223,7 +211,7 @@ class DeserializeNeedingCarpentryTests { "gen.Interface", mapOf("age" to NonNullableField (Int::class.java)))) - val concreteClass = cc.build (ClassSchema ("gen.Class", mapOf( + val concreteClass = cc.build (ClassSchema (testName(), mapOf( "age" to NonNullableField (Int::class.java), "name" to NonNullableField(String::class.java)), interfaces = listOf (I::class.java, interfaceClass))) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index afb558ebdb..ff20bffe97 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp; +import net.corda.nodeapi.internal.serialization.AllWhitelist; import net.corda.core.serialization.SerializedBytes; import org.apache.qpid.proton.codec.DecoderImpl; import org.apache.qpid.proton.codec.EncoderImpl; @@ -167,7 +168,7 @@ public class JavaSerializationOutputTests { } private Object serdes(Object obj) throws NotSerializableException { - SerializerFactory factory = new SerializerFactory(); + SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); SerializationOutput ser = new SerializationOutput(factory); SerializedBytes bytes = ser.serialize(obj); diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 53bdaaafe1..3f4e534004 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -14,7 +14,6 @@ import net.corda.nodeapi.RPCException import net.corda.nodeapi.internal.serialization.AbstractAMQPSerializationScheme import net.corda.nodeapi.internal.serialization.EmptyWhitelist import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.isPrimitive -import net.corda.nodeapi.internal.serialization.amqp.custom.* import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY import org.apache.qpid.proton.amqp.* @@ -136,8 +135,9 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) private fun serdes(obj: Any, - factory: SerializerFactory = SerializerFactory(), - freshDeserializationFactory: SerializerFactory = SerializerFactory(), + factory: SerializerFactory = SerializerFactoryFactory.get(), + freshDeserializationFactory: SerializerFactory = SerializerFactory( + AllWhitelist, ClassLoader.getSystemClassLoader()), expectedEqual: Boolean = true, expectDeserializedEqual: Boolean = true): Any { val ser = SerializationOutput(factory) @@ -285,13 +285,13 @@ class SerializationOutputTests { @Test(expected = NotSerializableException::class) fun `test whitelist`() { val obj = Woo2(4) - serdes(obj, SerializerFactory(EmptyWhitelist)) + serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) } @Test fun `test annotation whitelisting`() { val obj = AnnotatedWoo(5) - serdes(obj, SerializerFactory(EmptyWhitelist)) + serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) } @Test(expected = NotSerializableException::class) @@ -387,10 +387,10 @@ class SerializationOutputTests { @Test fun `test custom serializers on public key`() { - val factory = SerializerFactory() - factory.register(PublicKeySerializer) - val factory2 = SerializerFactory() - factory2.register(PublicKeySerializer) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) val obj = MEGA_CORP_PUBKEY serdes(obj, factory, factory2) } @@ -398,16 +398,16 @@ class SerializationOutputTests { @Test fun `test annotation is inherited`() { val obj = InheritAnnotation("blah") - serdes(obj, SerializerFactory(EmptyWhitelist)) + serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) } @Test fun `test throwables serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) val t = IllegalAccessException("message").fillInStackTrace() val desThrowable = serdes(t, factory, factory2, false) as Throwable @@ -416,11 +416,11 @@ class SerializationOutputTests { @Test fun `test complex throwables serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) try { try { @@ -447,11 +447,11 @@ class SerializationOutputTests { @Test fun `test suppressed throwables serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) try { try { @@ -469,11 +469,11 @@ class SerializationOutputTests { @Test fun `test flow corda exception subclasses serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) val obj = FlowException("message").fillInStackTrace() serdes(obj, factory, factory2) @@ -481,11 +481,11 @@ class SerializationOutputTests { @Test fun `test RPC corda exception subclasses serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) val obj = RPCException("message").fillInStackTrace() serdes(obj, factory, factory2) @@ -525,7 +525,7 @@ class SerializationOutputTests { @Test fun `test transaction state`() { - val state = TransactionState(FooState(), MEGA_CORP) + val state = TransactionState(FooState(), MEGA_CORP) val factory = SerializerFactory() AbstractAMQPSerializationScheme.registerCustomSerializers(factory) @@ -542,11 +542,11 @@ class SerializationOutputTests { @Test fun `test currencies serialize`() { - val factory = SerializerFactory() - factory.register(CurrencySerializer) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer) - val factory2 = SerializerFactory() - factory2.register(CurrencySerializer) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer) val obj = Currency.getInstance("USD") serdes(obj, factory, factory2) @@ -554,11 +554,11 @@ class SerializationOutputTests { @Test fun `test big decimals serialize`() { - val factory = SerializerFactory() - factory.register(BigDecimalSerializer) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) - val factory2 = SerializerFactory() - factory2.register(BigDecimalSerializer) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) val obj = BigDecimal("100000000000000000000000000000.00") serdes(obj, factory, factory2) @@ -566,11 +566,11 @@ class SerializationOutputTests { @Test fun `test instants serialize`() { - val factory = SerializerFactory() - factory.register(InstantSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(InstantSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(factory2)) val obj = Instant.now() serdes(obj, factory, factory2) @@ -578,11 +578,11 @@ class SerializationOutputTests { @Test fun `test StateRef serialize`() { - val factory = SerializerFactory() - factory.register(InstantSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(InstantSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(factory2)) val obj = StateRef(SecureHash.randomSHA256(), 0) serdes(obj, factory, factory2) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt index 3aae840918..2dd62bff58 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt @@ -4,8 +4,8 @@ import net.corda.nodeapi.internal.serialization.amqp.Field import net.corda.nodeapi.internal.serialization.amqp.Schema import net.corda.nodeapi.internal.serialization.amqp.TypeNotation import net.corda.nodeapi.internal.serialization.amqp.CompositeType -import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactoryFactory fun mangleName(name: String) = "${name}__carpenter" @@ -37,7 +37,7 @@ fun Schema.mangleNames(names: List): Schema { } open class AmqpCarpenterBase { - var factory = SerializerFactory() + var factory = SerializerFactoryFactory.get() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) fun testName(): String = Thread.currentThread().stackTrace[2].methodName diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt index 5ec40e0f4e..a61bee62f7 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt @@ -63,7 +63,7 @@ class CompositeMembers : AmqpCarpenterBase() { assertEquals("b", amqpSchemaB.fields[1].name) assertEquals("int", amqpSchemaB.fields[1].type) - val metaSchema = obj.envelope.schema.carpenterSchema() + val metaSchema = obj.envelope.schema.carpenterSchema(ClassLoader.getSystemClassLoader()) // if we know all the classes there is nothing to really achieve here assert(metaSchema.carpenterSchemas.isEmpty()) @@ -92,7 +92,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is B) - amqpSchema.carpenterSchema() + amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) } @Test @@ -112,7 +112,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is B) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B"))) - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(1, carpenterSchema.size) @@ -140,7 +140,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is B) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // just verify we're in the expected initial state, A is carpentable, B is not because // it depends on A and the dependency chains are in place @@ -200,7 +200,7 @@ class CompositeMembers : AmqpCarpenterBase() { val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - amqpSchema.carpenterSchema() + amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) } @Test(expected = UncarpentableException::class) @@ -226,7 +226,7 @@ class CompositeMembers : AmqpCarpenterBase() { val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - amqpSchema.carpenterSchema() + amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) } @Suppress("UNUSED") @@ -251,7 +251,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is C) val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - TestMetaCarpenter(carpenterSchema.carpenterSchema()) + TestMetaCarpenter(carpenterSchema.carpenterSchema(ClassLoader.getSystemClassLoader())) } /* diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt index 8176823aa3..81cf727c58 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt @@ -45,14 +45,14 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertTrue(obj.obj is A) val serSchema = obj.envelope.schema assertEquals(2, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // since we're using an envelope generated by seilaising classes defined locally // it's extremely unlikely we'd need to carpent any classes assertEquals(0, l1.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(1, l2.size) val aSchema = l2.carpenterSchemas.find { it.name == mangleName(classTestName("A")) } @@ -88,13 +88,13 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(2, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(0, l1.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) val aName = mangleName(classTestName("A")) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(1, l2.size) @@ -132,7 +132,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(3, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // since we're using an envelope generated by serialising classes defined locally // it's extremely unlikely we'd need to carpent any classes @@ -141,7 +141,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { // pretend we don't know the class we've been sent, i.e. it's unknown to the class loader, and thus // needs some carpentry val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val aName = mangleName(classTestName("A")) assertEquals(1, l2.size) @@ -180,14 +180,14 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(3, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // since we're using an envelope generated by serialising classes defined locally // it's extremely unlikely we'd need to carpent any classes assertEquals(0, l1.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val aName = mangleName(classTestName("A")) assertEquals(1, l2.size) @@ -235,7 +235,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(4, serSchema.types.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - val cSchema = mangleSchema.carpenterSchema() + val cSchema = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val aName = mangleName(classTestName("A")) val bName = mangleName(classTestName("B")) @@ -292,7 +292,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { // ignore the return as we expect this to throw serSchema.mangleNames(listOf( - classTestName("A"), "${this.javaClass.`package`.name}.I")).carpenterSchema() + classTestName("A"), "${this.javaClass.`package`.name}.I")).carpenterSchema(ClassLoader.getSystemClassLoader()) } @Test @@ -315,7 +315,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { val amqpSchema = serSchema.mangleNames(listOf(classTestName("A"), "${this.javaClass.`package`.name}.I")) val aName = mangleName(classTestName("A")) val iName = mangleName("${this.javaClass.`package`.name}.I") - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // whilst there are two unknown classes within the envelope A depends on I so we can't construct a // schema for A until we have for I @@ -362,7 +362,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { val aName = mangleName(classTestName("A")) val iName = mangleName("${this.javaClass.`package`.name}.I") val iiName = mangleName("${this.javaClass.`package`.name}.II") - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // there is nothing preventing us from carpenting up the two interfaces so // our initial list should contain both interface with A being dependent on both @@ -414,7 +414,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { val aName = mangleName(classTestName("A")) val iName = mangleName("${this.javaClass.`package`.name}.I") val iiiName = mangleName("${this.javaClass.`package`.name}.III") - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // Since A depends on III and III extends I we will have to construct them // in that reverse order (I -> III -> A) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt index d823e5a8e7..f9ea362882 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt @@ -36,7 +36,10 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("int", amqpSchema.fields[1].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) assertEquals(1, carpenterSchema.size) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") } @@ -77,7 +80,10 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("string", amqpSchema.fields[1].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) assertEquals(1, carpenterSchema.size) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt index c68e222568..532e174903 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt @@ -30,7 +30,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("int", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -58,7 +61,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { val amqpSchema = obj.envelope.schema.types[0] as CompositeType val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -90,7 +96,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("long", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -122,7 +131,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("short", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -154,7 +166,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("double", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -186,7 +201,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("float", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) From 825908cf1537c86c8577d218b18be434b2f099e6 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 15 Aug 2017 14:14:21 +0100 Subject: [PATCH 002/101] Refactoring changes to work with interface changes Specifically we don't need the factory factory as that functionality lives in the global defaults --- .../serialization/AMQPSerializationScheme.kt | 5 +- .../serialization/KryoAMQPSerializer.kt | 4 -- .../serialization/amqp/CustomSerializer.kt | 49 +++++++++---------- .../amqp/DeserializationInput.kt | 2 +- .../serialization/amqp/SerializationOutput.kt | 2 +- .../amqp/SerializerFactoryFactory.kt | 22 --------- .../serialization/amqp/AMQPTestUtils.kt | 6 ++- .../amqp/DeserializeAndReturnEnvelopeTests.kt | 4 +- .../amqp/DeserializeSimpleTypesTests.kt | 22 ++++----- .../amqp/SerializationOutputTests.kt | 10 ++-- .../carpenter/ClassCarpenterTestUtils.kt | 7 +-- 11 files changed, 55 insertions(+), 78 deletions(-) delete mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index 5e5186c343..e0a54e8a8a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -47,8 +47,9 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { } fun getSerializerFactory(): SerializerFactory { - return serializerFactoriesForContexts.computeIfAbsent(Pair(AllWhitelist, deserializationClassLoader)) { - SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + return serializerFactoriesForContexts.computeIfAbsent(Pair( + AllWhitelist, SerializationDefaults.javaClass.classLoader)) { + SerializerFactory(AllWhitelist, SerializationDefaults.javaClass.classLoader) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt index dae649b285..90ea983388 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt @@ -6,13 +6,9 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.SerializedBytes import net.corda.core.utilities.sequence import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput -import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput -import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory -import net.corda.nodeapi.internal.serialization.amqp.SerializerFactoryFactory /** * This [Kryo] custom [Serializer] switches the object graph of anything annotated with `@CordaSerializable` diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 51911f2d24..8f602a65de 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.SerializationDefaults import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -68,26 +69,26 @@ abstract class CustomSerializer : AMQPSerializer { } /** - * Additional base features for a custom serializer for a particular class, that excludes subclasses. + * Additional base features for a custom serializer for a particular class [withInheritance] is false + * or super class / interfaces [withInheritance] is true */ - abstract class Is(protected val clazz: Class) : CustomSerializer() { - override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz + abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { override val type: Type get() = clazz override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" override fun writeClassInfo(output: SerializationOutput) {} override val descriptor: Descriptor = Descriptor(typeDescriptor) + override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz } + /** + * Additional base features for a custom serializer for a particular class, that excludes subclasses. + */ + abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) + /** * Additional base features for a custom serializer for all implementations of a particular interface or super class. */ - abstract class Implements(protected val clazz: Class) : CustomSerializer() { - override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz) - override val type: Type get() = clazz - override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" - override fun writeClassInfo(output: SerializationOutput) {} - override val descriptor: Descriptor = Descriptor(typeDescriptor) - } + abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) /** * Additional base features over and above [Implements] or [Is] custom serializer for when the serialized form should be @@ -96,15 +97,11 @@ abstract class CustomSerializer : AMQPSerializer { * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered * custom serializers. */ - abstract class Proxy(protected val clazz: Class, + abstract class Proxy(clazz: Class, protected val proxyClass: Class

, protected val factory: SerializerFactory, - val withInheritance: Boolean = true) : CustomSerializer() { + withInheritance: Boolean = true) : CustomSerializerImp(clazz, withInheritance) { override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz - override val type: Type get() = clazz - override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" - override fun writeClassInfo(output: SerializationOutput) {} - override val descriptor: Descriptor = Descriptor(typeDescriptor) private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) } @@ -152,26 +149,26 @@ abstract class CustomSerializer : AMQPSerializer { */ abstract class ToString(clazz: Class, withInheritance: Boolean = false, private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { - `constructor` -> { string -> `constructor`.newInstance(string) } + `constructor` -> + { string -> `constructor`.newInstance(string) } }, - private val unmaker: (T) -> String = { obj -> obj.toString() }) : Proxy(clazz, String::class.java, /* Unused */ SerializerFactoryFactory.get(), withInheritance) { + private val unmaker: (T) -> String = { obj -> obj.toString() }) + : CustomSerializerImp(clazz, withInheritance) { override val additionalSerializers: Iterable> = emptyList() - override val schemaForDocumentation = Schema(listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)), SerializerFactory.primitiveTypeName(String::class.java)!!, descriptor, emptyList()))) - - override fun toProxy(obj: T): String = unmaker(obj) - - override fun fromProxy(proxy: String): T = maker(proxy) + override val schemaForDocumentation = Schema( + listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)), + SerializerFactory.primitiveTypeName(String::class.java)!!, + descriptor, emptyList()))) override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) { - val proxy = toProxy(obj) - data.putObject(proxy) + data.putObject(unmaker(obj)) } override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { val proxy = input.readObject(obj, schema, String::class.java) as String - return fromProxy(proxy) + return maker(proxy) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 8764064116..621b01d509 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -20,7 +20,7 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -class DeserializationInput(internal val serializerFactory: SerializerFactory = SerializerFactoryFactory.get()) { +class DeserializationInput(internal val serializerFactory: SerializerFactory) { // TODO: we're not supporting object refs yet private val objectHistory: MutableList = ArrayList() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index 7e47932f06..39b96b9679 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -14,7 +14,7 @@ import kotlin.collections.LinkedHashSet * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -open class SerializationOutput(internal val serializerFactory: SerializerFactory = SerializerFactoryFactory.get()) { +open class SerializationOutput(internal val serializerFactory: SerializerFactory) { // TODO: we're not supporting object refs yet private val objectHistory: MutableMap = IdentityHashMap() private val serializerHistory: MutableSet> = LinkedHashSet() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt deleted file mode 100644 index 81a2643918..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactoryFactory.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.nodeapi.internal.serialization.amqp - -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.ClassWhitelist -import net.corda.nodeapi.internal.serialization.AllWhitelist - -/** - * Factory singleton that maps unique Serializer Factories from a pair of WhitleList and ClassLoader - */ -object SerializerFactoryFactory { - val factories : MutableMap, SerializerFactory> = mutableMapOf() - - fun get(context: SerializationContext) : SerializerFactory = - factories.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { - SerializerFactory(context.whitelist, context.deserializationClassLoader) - } - - fun get() : SerializerFactory = - factories.computeIfAbsent(Pair(AllWhitelist, ClassLoader.getSystemClassLoader())) { - SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) - } -} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt index c22a70f4d9..e1f7db12c8 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt @@ -1,10 +1,14 @@ package net.corda.nodeapi.internal.serialization.amqp import org.apache.qpid.proton.codec.Data +import net.corda.nodeapi.internal.serialization.AllWhitelist + +fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) class TestSerializationOutput( private val verbose: Boolean, - serializerFactory: SerializerFactory = SerializerFactoryFactory.get()) : SerializationOutput(serializerFactory) { + serializerFactory: SerializerFactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader())) + : SerializationOutput(serializerFactory) { override fun writeSchema(schema: Schema, data: Data) { if (verbose) println(schema) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt index 39ed38bea4..6747b15db9 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt @@ -18,7 +18,7 @@ class DeserializeAndReturnEnvelopeTests { val a = A(10, "20") - val factory = SerializerFactoryFactory.get() + val factory = testDefaultFactory() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) @@ -34,7 +34,7 @@ class DeserializeAndReturnEnvelopeTests { val b = B(A(10, "20"), 30.0F) - val factory = SerializerFactoryFactory.get() + val factory = testDefaultFactory() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt index 1e7171b31a..88ec97ca42 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt @@ -16,30 +16,30 @@ class DeserializeSimpleTypesTests { private const val VERBOSE = false } - val sf1 = SerializerFactory() - val sf2 = SerializerFactory() + val sf1 = testDefaultFactory() + val sf2 = testDefaultFactory() @Test fun testChar() { data class C(val c: Char) - var deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('c'))) + var deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('c'))) assertEquals('c', deserializedC.c) // CYRILLIC CAPITAL LETTER YU (U+042E) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('Ю'))) + deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('Ю'))) assertEquals('Ю', deserializedC.c) // ARABIC LETTER FEH WITH DOT BELOW (U+06A3) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('ڣ'))) + deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('ڣ'))) assertEquals('ڣ', deserializedC.c) // ARABIC LETTER DAD WITH DOT BELOW (U+06FB) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('ۻ'))) + deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('ۻ'))) assertEquals('ۻ', deserializedC.c) // BENGALI LETTER AA (U+0986) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('আ'))) + deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('আ'))) assertEquals('আ', deserializedC.c) } @@ -49,8 +49,8 @@ class DeserializeSimpleTypesTests { data class C(val c: Character) val c = C(Character('c')) - val serialisedC = SerializationOutput().serialize(c) - val deserializedC = DeserializationInput().deserialize(serialisedC) + val serialisedC = SerializationOutput(sf).serialize(c) + val deserializedC = DeserializationInput(sf).deserialize(serialisedC) assertEquals(c.c, deserializedC.c) } @@ -60,8 +60,8 @@ class DeserializeSimpleTypesTests { data class C(val c: Char?) val c = C(null) - val serialisedC = SerializationOutput().serialize(c) - val deserializedC = DeserializationInput().deserialize(serialisedC) + val serialisedC = SerializationOutput(sf).serialize(c) + val deserializedC = DeserializationInput(sf).deserialize(serialisedC) assertEquals(c.c, deserializedC.c) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 3f4e534004..70a68e8cce 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -14,6 +14,9 @@ import net.corda.nodeapi.RPCException import net.corda.nodeapi.internal.serialization.AbstractAMQPSerializationScheme import net.corda.nodeapi.internal.serialization.EmptyWhitelist import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.isPrimitive +import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl +import net.corda.nodeapi.internal.serialization.AMQPServerSerializationScheme +import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY import org.apache.qpid.proton.amqp.* @@ -135,7 +138,8 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) private fun serdes(obj: Any, - factory: SerializerFactory = SerializerFactoryFactory.get(), + factory: SerializerFactory = SerializerFactory ( + AllWhitelist, ClassLoader.getSystemClassLoader()), freshDeserializationFactory: SerializerFactory = SerializerFactory( AllWhitelist, ClassLoader.getSystemClassLoader()), expectedEqual: Boolean = true, @@ -527,10 +531,10 @@ class SerializationOutputTests { fun `test transaction state`() { val state = TransactionState(FooState(), MEGA_CORP) - val factory = SerializerFactory() + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) AbstractAMQPSerializationScheme.registerCustomSerializers(factory) - val factory2 = SerializerFactory() + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) AbstractAMQPSerializationScheme.registerCustomSerializers(factory2) val desState = serdes(state, factory, factory2, expectedEqual = false, expectDeserializedEqual = false) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt index 2dd62bff58..b7f1c2d348 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt @@ -1,11 +1,8 @@ package net.corda.nodeapi.internal.serialization.carpenter +import net.corda.nodeapi.internal.serialization.amqp.* import net.corda.nodeapi.internal.serialization.amqp.Field import net.corda.nodeapi.internal.serialization.amqp.Schema -import net.corda.nodeapi.internal.serialization.amqp.TypeNotation -import net.corda.nodeapi.internal.serialization.amqp.CompositeType -import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput -import net.corda.nodeapi.internal.serialization.amqp.SerializerFactoryFactory fun mangleName(name: String) = "${name}__carpenter" @@ -37,7 +34,7 @@ fun Schema.mangleNames(names: List): Schema { } open class AmqpCarpenterBase { - var factory = SerializerFactoryFactory.get() + var factory = testDefaultFactory() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) fun testName(): String = Thread.currentThread().stackTrace[2].methodName From 49c23a58d14fb3effc4350bfcd6800f8828a54df Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 15 Aug 2017 15:08:41 +0100 Subject: [PATCH 003/101] Final changes integrating tests into the new API Fix some rebase issues Ensure tests are using the correct versions of the factories --- .../serialization/amqp/SerializerFactory.kt | 15 ++++++------ ...erializeNeedingCarpentrySimpleTypesTest.kt | 4 ++-- .../amqp/DeserializeNeedingCarpentryTests.kt | 23 ++++++++++++++++--- .../amqp/DeserializeSimpleTypesTests.kt | 18 +++++++-------- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index b88c89d99c..363909d0b3 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -4,7 +4,6 @@ import com.google.common.primitives.Primitives import com.google.common.reflect.TypeResolver import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable -import net.corda.nodeapi.internal.serialization.AllWhitelist import net.corda.nodeapi.internal.serialization.carpenter.CarpenterSchemas import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter import net.corda.nodeapi.internal.serialization.carpenter.MetaCarpenter @@ -182,13 +181,13 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { * Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd * if not use the [ClassCarpenter] to generate a class to use in it's place */ - private fun processSchema(schema: Schema, sentinal: Boolean = false) { + private fun processSchema(schema: Schema, sentinel: Boolean = false) { val carpenterSchemas = CarpenterSchemas.newInstance() for (typeNotation in schema.types) { try { - } - catch (e: ClassNotFoundException) { - if (sentinal || (typeNotation !is CompositeType)) throw e + processSchemaEntry(typeNotation) + } catch (e: ClassNotFoundException) { + if (sentinel || (typeNotation !is CompositeType)) throw e typeNotation.carpenterSchema(classloader, carpenterSchemas = carpenterSchemas) } } @@ -209,13 +208,13 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { private fun processRestrictedType(typeNotation: RestrictedType) { // TODO: class loader logic, and compare the schema. - val type = typeForName(typeNotation.name) + val type = typeForName(typeNotation.name, classloader) get(null, type) } - private fun processCompositeType(typeNotation: CompositeType, + private fun processCompositeType(typeNotation: CompositeType) { // TODO: class loader logic, and compare the schema. - val type = typeForName(typeNotation.name, cl) + val type = typeForName(typeNotation.name, classloader) get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt index 01fb68a22f..0a05542ad7 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt @@ -16,8 +16,8 @@ class DeserializeNeedingCarpentrySimpleTypesTest : AmqpCarpenterBase() { private const val VERBOSE = false } - val sf = SerializerFactory() - val sf2 = SerializerFactory() + val sf = testDefaultFactory() + val sf2 = testDefaultFactory() @Test fun singleInt() { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt index d8feeb8473..585a362d15 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt @@ -25,8 +25,8 @@ class DeserializeNeedingCarpentryTests : AmqpCarpenterBase() { private const val VERBOSE = false } - val sf1 = SerializerFactory() - val sf2 = SerializerFactory() + val sf1 = testDefaultFactory() + val sf2 = testDefaultFactory() @Test fun verySimpleType() { @@ -165,7 +165,7 @@ class DeserializeNeedingCarpentryTests : AmqpCarpenterBase() { @Test fun nestedTypes() { val cc = ClassCarpenter() - val nestedClass = cc.build (ClassSchema(testName(), + val nestedClass = cc.build (ClassSchema("nestedType", mapOf("name" to NonNullableField(String::class.java)))) val outerClass = cc.build (ClassSchema("outerType", @@ -174,6 +174,23 @@ class DeserializeNeedingCarpentryTests : AmqpCarpenterBase() { val classInstance = outerClass.constructors.first().newInstance(nestedClass.constructors.first().newInstance("name")) val serialisedBytes = TestSerializationOutput(VERBOSE, sf1).serialize(classInstance) val deserializedObj = DeserializationInput(sf2).deserialize(serialisedBytes) + + val inner = deserializedObj::class.java.getMethod("getInner").invoke(deserializedObj) + assertEquals("name", inner::class.java.getMethod("getName").invoke(inner)) + } + + @Test + fun repeatedNestedTypes() { + val cc = ClassCarpenter() + val nestedClass = cc.build (ClassSchema("nestedType", + mapOf("name" to NonNullableField(String::class.java)))) + + data class outer(val a: Any, val b: Any) + + val classInstance = outer ( + nestedClass.constructors.first().newInstance("foo"), + nestedClass.constructors.first().newInstance("bar")) + val serialisedBytes = TestSerializationOutput(VERBOSE, sf1).serialize(classInstance) val deserializedObj = DeserializationInput(sf2).deserialize(serialisedBytes) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt index 88ec97ca42..cc05b9b39f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt @@ -23,23 +23,23 @@ class DeserializeSimpleTypesTests { fun testChar() { data class C(val c: Char) - var deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('c'))) + var deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('c'))) assertEquals('c', deserializedC.c) // CYRILLIC CAPITAL LETTER YU (U+042E) - deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('Ю'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('Ю'))) assertEquals('Ю', deserializedC.c) // ARABIC LETTER FEH WITH DOT BELOW (U+06A3) - deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('ڣ'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('ڣ'))) assertEquals('ڣ', deserializedC.c) // ARABIC LETTER DAD WITH DOT BELOW (U+06FB) - deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('ۻ'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('ۻ'))) assertEquals('ۻ', deserializedC.c) // BENGALI LETTER AA (U+0986) - deserializedC = DeserializationInput(sf).deserialize(SerializationOutput(sf).serialize(C('আ'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('আ'))) assertEquals('আ', deserializedC.c) } @@ -49,8 +49,8 @@ class DeserializeSimpleTypesTests { data class C(val c: Character) val c = C(Character('c')) - val serialisedC = SerializationOutput(sf).serialize(c) - val deserializedC = DeserializationInput(sf).deserialize(serialisedC) + val serialisedC = SerializationOutput(sf1).serialize(c) + val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) assertEquals(c.c, deserializedC.c) } @@ -60,8 +60,8 @@ class DeserializeSimpleTypesTests { data class C(val c: Char?) val c = C(null) - val serialisedC = SerializationOutput(sf).serialize(c) - val deserializedC = DeserializationInput(sf).deserialize(serialisedC) + val serialisedC = SerializationOutput(sf1).serialize(c) + val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) assertEquals(c.c, deserializedC.c) } From 62b26bcd897e373cbac5f7c2b600b6ded340f91d Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 14 Aug 2017 19:03:24 +0100 Subject: [PATCH 004/101] Moved Currency stuff in ContractsDSL out of core and into finance --- .../net/corda/jackson/JacksonSupport.kt | 3 +- .../net/corda/jackson/JacksonSupportTest.kt | 5 +- .../corda/client/jfx/NodeMonitorModelTest.kt | 4 +- .../net/corda/client/mock/EventGenerator.kt | 4 +- .../client/rpc/CordaRPCJavaClientTest.java | 18 ++-- .../corda/client/rpc/CordaRPCClientTest.kt | 4 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 5 +- .../kotlin/net/corda/core/contracts/Amount.kt | 62 +----------- .../net/corda/core/contracts/ContractsDSL.kt | 34 +------ .../net/corda/core/utilities/KotlinUtils.kt | 10 +- .../net/corda/core/contracts/AmountTests.kt | 33 +------ .../contracts/TransactionEncumbranceTests.kt | 2 + .../core/crypto/PartialMerkleTreeTest.kt | 2 + .../core/flows/ContractUpgradeFlowTest.kt | 2 + .../net/corda/core/flows/FinalityFlowTests.kt | 2 +- .../core/flows/ManualFinalityFlowTests.kt | 2 +- .../TransactionSerializationTests.kt | 1 + docs/source/changelog.rst | 2 + .../corda/docs/IntegrationTestingTutorial.kt | 3 +- .../net/corda/docs/ClientRpcTutorial.kt | 2 +- .../net/corda/docs/CustomVaultQueryTest.kt | 1 + .../docs/FxTransactionBuildTutorialTest.kt | 2 +- .../net/corda/contracts/universal/Examples.kt | 2 +- .../kotlin/net/corda/finance/CurrencyUtils.kt | 96 +++++++++++++++++++ .../kotlin/net/corda/flows/CashExitFlow.kt | 2 +- .../kotlin/net/corda/flows/CashIssueFlow.kt | 2 +- .../main/kotlin/net/corda/flows/IssuerFlow.kt | 8 +- .../corda/contracts/asset/CashTestsJava.java | 4 +- .../corda/contracts/CommercialPaperTests.kt | 2 + .../net/corda/contracts/asset/CashTests.kt | 1 + .../corda/contracts/asset/ObligationTests.kt | 1 + .../net/corda/finance/CurrencyUtilsTest.kt | 30 ++++++ .../net/corda/flows/CashExitFlowTests.kt | 4 +- .../net/corda/flows/CashIssueFlowTests.kt | 4 +- .../net/corda/flows/CashPaymentFlowTests.kt | 4 +- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 9 +- .../net/corda/node/NodePerformanceTests.kt | 2 +- .../node/services/DistributedServiceTests.kt | 2 +- .../net/corda/node/CordaRPCOpsImplTest.kt | 3 + .../node/messaging/TwoPartyTradeFlowTests.kt | 2 + .../database/HibernateConfigurationTest.kt | 3 + .../persistence/DataVendingServiceTests.kt | 2 +- .../statemachine/FlowFrameworkTests.kt | 2 +- .../services/vault/NodeVaultServiceTest.kt | 1 + .../node/services/vault/VaultQueryTests.kt | 1 + .../node/services/vault/VaultWithCashTest.kt | 5 +- .../corda/bank/BankOfCordaRPCClientTest.kt | 3 +- .../corda/bank/api/BankOfCordaClientApi.kt | 7 +- .../net/corda/bank/api/BankOfCordaWebApi.kt | 8 +- .../corda/irs/api/NodeInterestRatesTest.kt | 1 + .../kotlin/net/corda/irs/contract/IRSTests.kt | 9 +- .../notarydemo/flows/DummyIssueAndMove.kt | 1 + .../net/corda/traderdemo/TraderDemoTest.kt | 17 ++-- .../kotlin/net/corda/traderdemo/TraderDemo.kt | 7 +- .../corda/traderdemo/TraderDemoClientApi.kt | 13 +-- .../flow/CommercialPaperIssueFlow.kt | 4 +- .../net/corda/explorer/ExplorerSimulation.kt | 4 +- .../net/corda/explorer/model/IssuerModel.kt | 4 +- .../explorer/model/ReportingCurrencyModel.kt | 4 + .../net/corda/explorer/model/SettingsModel.kt | 3 +- .../corda/explorer/model/IssuerModelTest.kt | 24 +++-- .../net/corda/loadtest/tests/CrossCashTest.kt | 2 +- .../net/corda/loadtest/tests/SelfIssueTest.kt | 2 +- .../net/corda/loadtest/tests/StabilityTest.kt | 2 +- .../net/corda/verifier/VerifierTests.kt | 2 +- 65 files changed, 278 insertions(+), 234 deletions(-) create mode 100644 finance/src/main/kotlin/net/corda/finance/CurrencyUtils.kt create mode 100644 finance/src/test/kotlin/net/corda/finance/CurrencyUtilsTest.kt diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index ee8333f06e..907484e477 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -29,6 +29,7 @@ import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.parseCurrency import net.i2p.crypto.eddsa.EdDSAPublicKey import org.bouncycastle.asn1.x500.X500Name import java.math.BigDecimal @@ -348,7 +349,7 @@ object JacksonSupport { object AmountDeserializer : JsonDeserializer>() { override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> { try { - return Amount.parseCurrency(parser.text) + return parseCurrency(parser.text) } catch (e: Exception) { try { val tree = parser.readValueAsTree() diff --git a/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt index a92edb7aea..baa47ce900 100644 --- a/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt @@ -2,12 +2,13 @@ package net.corda.jackson import com.fasterxml.jackson.databind.SerializationFeature import net.corda.core.contracts.Amount -import net.corda.core.contracts.USD +import net.corda.finance.USD import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.generateKeyPair import net.corda.core.transactions.SignedTransaction +import net.corda.finance.parseCurrency import net.corda.testing.ALICE_PUBKEY import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MINI_CORP @@ -52,7 +53,7 @@ class JacksonSupportTest : TestDependencyInjectionBase() { @Test fun writeAmount() { val writer = mapper.writer().without(SerializationFeature.INDENT_OUTPUT) - assertEquals("""{"notional":"25000000.00 USD"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("$25000000")))) + assertEquals("""{"notional":"25000000.00 USD"}""", writer.writeValueAsString(Dummy(parseCurrency("$25000000")))) } @Test diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 94662d7378..797d857bd6 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -5,8 +5,8 @@ import net.corda.client.jfx.model.ProgressTrackingEvent import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.USD +import net.corda.finance.DOLLARS +import net.corda.finance.USD import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowInitiator diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index 6bd7b68e7c..5d8f0cd59f 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -1,8 +1,8 @@ package net.corda.client.mock import net.corda.core.contracts.Amount -import net.corda.core.contracts.GBP -import net.corda.core.contracts.USD +import net.corda.finance.GBP +import net.corda.finance.USD import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.flows.CashFlowCommand diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index 9f38c17316..d849f923b1 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -1,7 +1,7 @@ package net.corda.client.rpc; -import net.corda.core.concurrent.CordaFuture; import net.corda.client.rpc.internal.RPCClient; +import net.corda.core.concurrent.CordaFuture; import net.corda.core.contracts.Amount; import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.FlowHandle; @@ -22,9 +22,13 @@ import java.io.IOException; import java.util.*; import java.util.concurrent.ExecutionException; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static kotlin.test.AssertionsKt.assertEquals; import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault; import static net.corda.contracts.GetBalances.getCashBalance; +import static net.corda.finance.CurrencyUtils.DOLLARS; import static net.corda.node.services.RPCUserServiceKt.startFlowPermission; import static net.corda.testing.TestConstants.getALICE; @@ -45,10 +49,10 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws ExecutionException, InterruptedException { - Set services = new HashSet<>(Collections.singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null))); - CordaFuture nodeFuture = startNode(getALICE().getName(), 1, services, Arrays.asList(rpcUser), Collections.emptyMap()); + Set services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null))); + CordaFuture nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap()); node = nodeFuture.get(); - client = new CordaRPCClient(node.getConfiguration().getRpcAddress(), null, getDefault(), false); + client = new CordaRPCClient(requireNonNull(node.getConfiguration().getRpcAddress()), null, getDefault(), false); } @After @@ -65,10 +69,8 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException { login(rpcUser.getUsername(), rpcUser.getPassword()); - Amount dollars123 = new Amount<>(123, Currency.getInstance("USD")); - FlowHandle flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, - dollars123, OpaqueBytes.of("1".getBytes()), + DOLLARS(123), OpaqueBytes.of("1".getBytes()), node.info.getLegalIdentity(), node.info.getLegalIdentity()); System.out.println("Started issuing cash, waiting on result"); flowHandle.getReturnValue().get(); @@ -76,6 +78,6 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { Amount balance = getCashBalance(rpcProxy, Currency.getInstance("USD")); System.out.print("Balance: " + balance + "\n"); - assertEquals(dollars123, balance, "matching"); + assertEquals(DOLLARS(123), balance, "matching"); } } diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 19e5fdc50e..8ef546ab54 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -2,8 +2,8 @@ package net.corda.client.rpc import net.corda.contracts.getCashBalance import net.corda.contracts.getCashBalances -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.USD +import net.corda.finance.DOLLARS +import net.corda.finance.USD import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowInitiator import net.corda.core.messaging.* diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 064db6fc7d..39295148e4 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -7,7 +7,6 @@ import net.corda.client.rpc.notUsed import net.corda.contracts.asset.Cash import net.corda.contracts.getCashBalance import net.corda.contracts.getCashBalances -import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.internal.InputStreamAndHash import net.corda.core.messaging.* @@ -18,6 +17,10 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds +import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS +import net.corda.finance.SWISS_FRANCS +import net.corda.finance.USD import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.nodeapi.User diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index e08ddce671..d74ea62be3 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -1,9 +1,9 @@ package net.corda.core.contracts import net.corda.core.crypto.composite.CompositeKey -import net.corda.core.utilities.exactAdd import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.exactAdd import java.math.BigDecimal import java.math.RoundingMode import java.util.* @@ -85,66 +85,6 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, } return BigDecimal.ONE } - - private val currencySymbols: Map = mapOf( - "$" to USD, - "£" to GBP, - "€" to EUR, - "¥" to JPY, - "₽" to RUB - ) - private val currencyCodes: Map by lazy { Currency.getAvailableCurrencies().map { it.currencyCode to it }.toMap() } - - /** - * Returns an amount that is equal to the given currency amount in text. Examples of what is supported: - * - * - 12 USD - * - 14.50 USD - * - 10 USD - * - 30 CHF - * - $10.24 - * - £13 - * - €5000 - * - * Note this method does NOT respect internationalisation rules: it ignores commas and uses . as the - * decimal point separator, always. It also ignores the users locale: - * - * - $ is always USD, - * - £ is always GBP - * - € is always the Euro - * - ¥ is always Japanese Yen. - * - ₽ is always the Russian ruble. - * - * Thus an input of $12 expecting some other countries dollar will not work. Do your own parsing if - * you need correct handling of currency amounts with locale-sensitive handling. - * - * @throws IllegalArgumentException if the input string was not understood. - */ - fun parseCurrency(input: String): Amount { - val i = input.filter { it != ',' } - try { - // First check the symbols at the front. - for ((symbol, currency) in currencySymbols) { - if (i.startsWith(symbol)) { - val rest = i.substring(symbol.length) - return fromDecimal(BigDecimal(rest), currency) - } - } - // Now check the codes at the end. - val split = i.split(' ') - if (split.size == 2) { - val (rest, code) = split - for ((cc, currency) in currencyCodes) { - if (cc == code) { - return fromDecimal(BigDecimal(rest), currency) - } - } - } - } catch(e: Exception) { - throw IllegalArgumentException("Could not parse $input as a currency", e) - } - throw IllegalArgumentException("Did not recognise the currency in $input or could not parse") - } } init { diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index fd38e9aa99..c1d43020f3 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -4,7 +4,6 @@ package net.corda.core.contracts import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import java.math.BigDecimal import java.security.PublicKey import java.util.* @@ -12,41 +11,10 @@ import java.util.* * Defines a simple domain specific language for the specification of financial contracts. Currently covers: * * - Some utilities for working with commands. - * - Code for working with currencies. - * - An Amount type that represents a positive quantity of a specific currency. + * - An Amount type that represents a positive quantity of a specific token. * - A simple language extension for specifying requirements in English, along with logic to enforce them. - * - * TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354) */ -//// Currencies /////////////////////////////////////////////////////////////////////////////////////////////////////// - -fun currency(code: String) = Currency.getInstance(code)!! - -@JvmField val USD = currency("USD") -@JvmField val GBP = currency("GBP") -@JvmField val EUR = currency("EUR") -@JvmField val CHF = currency("CHF") -@JvmField val JPY = currency("JPY") -@JvmField val RUB = currency("RUB") - -fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) -fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) -fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) -fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) -fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) -fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) - -val Int.DOLLARS: Amount get() = DOLLARS(this) -val Double.DOLLARS: Amount get() = DOLLARS(this) -val Int.POUNDS: Amount get() = POUNDS(this) -val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) - -infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) -infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit)) - //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// object Requirements { diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 16f29be8b7..2870b34abf 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -43,31 +43,31 @@ inline fun Logger.debug(msg: () -> String) { * Extension method for easier construction of [Duration]s in terms of integer days: `val twoDays = 2.days`. * @see Duration.ofDays */ -inline val Int.days: Duration get() = Duration.ofDays(toLong()) +val Int.days: Duration get() = Duration.ofDays(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer hours: `val twoHours = 2.hours`. * @see Duration.ofHours */ -inline val Int.hours: Duration get() = Duration.ofHours(toLong()) +val Int.hours: Duration get() = Duration.ofHours(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer minutes: `val twoMinutes = 2.minutes`. * @see Duration.ofMinutes */ -inline val Int.minutes: Duration get() = Duration.ofMinutes(toLong()) +val Int.minutes: Duration get() = Duration.ofMinutes(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer seconds: `val twoSeconds = 2.seconds`. * @see Duration.ofSeconds */ -inline val Int.seconds: Duration get() = Duration.ofSeconds(toLong()) +val Int.seconds: Duration get() = Duration.ofSeconds(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer milliseconds: `val twoMillis = 2.millis`. * @see Duration.ofMillis */ -inline val Int.millis: Duration get() = Duration.ofMillis(toLong()) +val Int.millis: Duration get() = Duration.ofMillis(toLong()) /** * A simple wrapper that enables the use of Kotlin's `val x by transient { ... }` syntax. Such a property diff --git a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt index 4c53791c8f..1a39b9ed49 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt @@ -1,5 +1,6 @@ package net.corda.core.contracts +import net.corda.finance.* import org.junit.Test import java.math.BigDecimal import java.util.* @@ -13,13 +14,6 @@ import kotlin.test.assertTrue * Tests of the [Amount] class. */ class AmountTests { - @Test - fun basicCurrency() { - val expected = 1000L - val amount = Amount(expected, GBP) - assertEquals(expected, amount.quantity) - } - @Test fun `make sure Amount has decimal places`() { val x = Amount(1, Currency.getInstance("USD")) @@ -27,7 +21,7 @@ class AmountTests { } @Test - fun decimalConversion() { + fun `decimal conversion`() { val quantity = 1234L val amountGBP = Amount(quantity, GBP) val expectedGBP = BigDecimal("12.34") @@ -49,22 +43,6 @@ class AmountTests { override fun toString(): String = name } - @Test - fun parsing() { - assertEquals(Amount(1234L, GBP), Amount.parseCurrency("£12.34")) - assertEquals(Amount(1200L, GBP), Amount.parseCurrency("£12")) - assertEquals(Amount(1000L, USD), Amount.parseCurrency("$10")) - assertEquals(Amount(5000L, JPY), Amount.parseCurrency("¥5000")) - assertEquals(Amount(500000L, RUB), Amount.parseCurrency("₽5000")) - assertEquals(Amount(1500000000L, CHF), Amount.parseCurrency("15,000,000 CHF")) - } - - @Test - fun rendering() { - assertEquals("5000 JPY", Amount.parseCurrency("¥5000").toString()) - assertEquals("50.12 USD", Amount.parseCurrency("$50.12").toString()) - } - @Test fun split() { for (baseQuantity in 0..1000) { @@ -81,7 +59,7 @@ class AmountTests { } @Test - fun amountTransfersEquality() { + fun `amount transfers equality`() { val partyA = "A" val partyB = "B" val partyC = "C" @@ -106,7 +84,7 @@ class AmountTests { } @Test - fun amountTransferAggregation() { + fun `amount transfer aggregation`() { val partyA = "A" val partyB = "B" val partyC = "C" @@ -137,7 +115,7 @@ class AmountTests { } @Test - fun amountTransferApply() { + fun `amount transfer apply`() { val partyA = "A" val partyB = "B" val partyC = "C" @@ -182,6 +160,5 @@ class AmountTests { assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)]) assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)]) assertEquals(originalTotals[Pair(partyB, GBP)], newTotals3[Pair(partyB, GBP)]) - } } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt index 2d05c6ea3d..94cbfc9ca2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt @@ -4,6 +4,8 @@ import net.corda.contracts.asset.Cash import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` import net.corda.testing.MEGA_CORP import net.corda.testing.MINI_CORP import net.corda.testing.ledger diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index 84fcc0f9e8..cfaf667c7e 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -8,6 +8,8 @@ import net.corda.core.identity.Party import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.WireTransaction +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` import net.corda.testing.* import org.junit.Test import java.security.PublicKey diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index bc05d5be77..d67cd2b010 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -14,6 +14,8 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.internal.Emoji import net.corda.core.utilities.getOrThrow +import net.corda.finance.USD +import net.corda.finance.`issued by` import net.corda.flows.CashIssueFlow import net.corda.node.internal.CordaRPCOpsImpl import net.corda.node.services.startFlowPermission diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index b280db5dff..1df25ee528 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -2,7 +2,7 @@ package net.corda.core.flows import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.GBP +import net.corda.finance.GBP import net.corda.core.contracts.Issued import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder diff --git a/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt index 01e81137f2..2ee3e17753 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt @@ -2,7 +2,7 @@ package net.corda.core.flows import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.GBP +import net.corda.finance.GBP import net.corda.core.contracts.Issued import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 492b50826f..7cc959190e 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -6,6 +6,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds +import net.corda.finance.POUNDS import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Before diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c0b60571e1..6eb3c270ef 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -40,6 +40,8 @@ UNRELEASED If you specifically need well known identities, use the network map, which is the authoritative source of current well known identities. +* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`. + Milestone 14 ------------ diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index fc04ddf82d..aec0c46e78 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -2,13 +2,12 @@ package net.corda.docs import net.corda.contracts.asset.Cash import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.DOLLARS +import net.corda.finance.DOLLARS import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.flows.CashIssueFlow diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index fe2255b3e7..e5279ef579 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -3,7 +3,7 @@ package net.corda.docs import net.corda.client.rpc.notUsed import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.USD +import net.corda.finance.USD import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultQueryBy diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 1995668fb0..7fbce2ed2e 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.* import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.finance.* import net.corda.flows.CashIssueFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index d937aaaa65..b860e43c02 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -1,11 +1,11 @@ package net.corda.docs import net.corda.contracts.getCashBalances -import net.corda.core.contracts.* import net.corda.core.node.services.ServiceInfo import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.finance.* import net.corda.flows.CashIssueFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Examples.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Examples.kt index f41f81b9bf..71b1ab347c 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Examples.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Examples.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.contracts.USD +import net.corda.finance.USD import org.junit.Ignore import org.junit.Test diff --git a/finance/src/main/kotlin/net/corda/finance/CurrencyUtils.kt b/finance/src/main/kotlin/net/corda/finance/CurrencyUtils.kt new file mode 100644 index 0000000000..2a566aacb7 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/CurrencyUtils.kt @@ -0,0 +1,96 @@ +@file:JvmName("CurrencyUtils") + +package net.corda.finance + +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.PartyAndReference +import java.math.BigDecimal +import java.util.* + +@JvmField val USD: Currency = Currency.getInstance("USD") +@JvmField val GBP: Currency = Currency.getInstance("GBP") +@JvmField val EUR: Currency = Currency.getInstance("EUR") +@JvmField val CHF: Currency = Currency.getInstance("CHF") +@JvmField val JPY: Currency = Currency.getInstance("JPY") +@JvmField val RUB: Currency = Currency.getInstance("RUB") + +fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) +fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) +fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) +fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) +fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) +fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) + +val Int.DOLLARS: Amount get() = DOLLARS(this) +val Double.DOLLARS: Amount get() = DOLLARS(this) +val Int.POUNDS: Amount get() = POUNDS(this) +val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) + +infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) +infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit)) + +private val currencySymbols: Map = mapOf( + "$" to USD, + "£" to GBP, + "€" to EUR, + "¥" to JPY, + "₽" to RUB +) + +private val currencyCodes: Map by lazy { + Currency.getAvailableCurrencies().associateBy { it.currencyCode } +} + +/** + * Returns an amount that is equal to the given currency amount in text. Examples of what is supported: + * + * - 12 USD + * - 14.50 USD + * - 10 USD + * - 30 CHF + * - $10.24 + * - £13 + * - €5000 + * + * Note this method does NOT respect internationalisation rules: it ignores commas and uses . as the + * decimal point separator, always. It also ignores the users locale: + * + * - $ is always USD, + * - £ is always GBP + * - € is always the Euro + * - ¥ is always Japanese Yen. + * - ₽ is always the Russian ruble. + * + * Thus an input of $12 expecting some other countries dollar will not work. Do your own parsing if + * you need correct handling of currency amounts with locale-sensitive handling. + * + * @throws IllegalArgumentException if the input string was not understood. + */ +fun parseCurrency(input: String): Amount { + val i = input.filter { it != ',' } + try { + // First check the symbols at the front. + for ((symbol, currency) in currencySymbols) { + if (i.startsWith(symbol)) { + val rest = i.substring(symbol.length) + return Amount.fromDecimal(BigDecimal(rest), currency) + } + } + // Now check the codes at the end. + val split = i.split(' ') + if (split.size == 2) { + val (rest, code) = split + for ((cc, currency) in currencyCodes) { + if (cc == code) { + return Amount.fromDecimal(BigDecimal(rest), currency) + } + } + } + } catch(e: Exception) { + throw IllegalArgumentException("Could not parse $input as a currency", e) + } + throw IllegalArgumentException("Did not recognise the currency in $input or could not parse") +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index abf0b511c8..5837eaf383 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.InsufficientBalanceException -import net.corda.core.contracts.issuedBy +import net.corda.finance.issuedBy import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.node.services.queryBy diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index 99304f122c..29d51996f5 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -3,7 +3,7 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.issuedBy +import net.corda.finance.issuedBy import net.corda.core.flows.FinalityFlow import net.corda.core.flows.StartableByRPC import net.corda.core.flows.TransactionKeyFlow diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index ccd6001fdf..51c885d176 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -2,7 +2,9 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.Issued import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable @@ -10,6 +12,10 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap +import net.corda.finance.CHF +import net.corda.finance.EUR +import net.corda.finance.GBP +import net.corda.finance.USD import java.util.* /** diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index ba479db499..4b7f2e07a7 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -6,8 +6,8 @@ import net.corda.core.identity.AnonymousParty; import net.corda.core.utilities.OpaqueBytes; import org.junit.Test; -import static net.corda.core.contracts.ContractsDSL.DOLLARS; -import static net.corda.core.contracts.ContractsDSL.issuedBy; +import static net.corda.finance.CurrencyUtils.DOLLARS; +import static net.corda.finance.CurrencyUtils.issuedBy; import static net.corda.testing.CoreTestUtils.*; /** diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 4f45920180..9bc8c2beb7 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -10,6 +10,8 @@ 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.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index c2ab2d3d75..7c2d05c3c0 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -11,6 +11,7 @@ import net.corda.core.node.services.queryBy import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.* import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index d661b0f7ab..e46c26259f 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -13,6 +13,7 @@ import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.days import net.corda.core.utilities.hours +import net.corda.finance.* import net.corda.testing.* import net.corda.testing.contracts.DummyState import net.corda.testing.node.MockServices diff --git a/finance/src/test/kotlin/net/corda/finance/CurrencyUtilsTest.kt b/finance/src/test/kotlin/net/corda/finance/CurrencyUtilsTest.kt new file mode 100644 index 0000000000..6e81ffc478 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/CurrencyUtilsTest.kt @@ -0,0 +1,30 @@ +package net.corda.finance + +import net.corda.core.contracts.* +import org.junit.Test +import kotlin.test.assertEquals + +class CurrencyUtilsTest { + @Test + fun `basic currency`() { + val expected = 1000L + val amount = Amount(expected, GBP) + assertEquals(expected, amount.quantity) + } + + @Test + fun parseCurrency() { + assertEquals(Amount(1234L, GBP), parseCurrency("£12.34")) + assertEquals(Amount(1200L, GBP), parseCurrency("£12")) + assertEquals(Amount(1000L, USD), parseCurrency("$10")) + assertEquals(Amount(5000L, JPY), parseCurrency("¥5000")) + assertEquals(Amount(500000L, RUB), parseCurrency("₽5000")) + assertEquals(Amount(1500000000L, CHF), parseCurrency("15,000,000 CHF")) + } + + @Test + fun rendering() { + assertEquals("5000 JPY", parseCurrency("¥5000").toString()) + assertEquals("50.12 USD", parseCurrency("$50.12").toString()) + } +} \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt index ed967899a2..44688436d6 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -1,8 +1,8 @@ package net.corda.flows import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.`issued by` +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt index 962632c4fc..9e56aa7105 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -1,8 +1,8 @@ package net.corda.flows import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.`issued by` +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index e056758ffb..a934d92127 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -1,8 +1,8 @@ package net.corda.flows import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.`issued by` +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.trackBy diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 2edfca7884..5a76d09aa7 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -2,19 +2,18 @@ package net.corda.flows import net.corda.contracts.asset.Cash import net.corda.core.concurrent.CordaFuture -import net.corda.testing.contracts.calculateRandomlySizedAmounts import net.corda.core.contracts.Amount -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.currency import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.trackBy import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS import net.corda.flows.IssuerFlow.IssuanceRequester +import net.corda.testing.contracts.calculateRandomlySizedAmounts import net.corda.testing.expect import net.corda.testing.expectEvents import net.corda.testing.node.MockNetwork @@ -109,7 +108,7 @@ class IssuerFlowTest(val anonymous: Boolean) { val notary = notaryNode.services.myInfo.notaryIdentity // try to issue an amount of a restricted currency assertFailsWith { - runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), + runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, Currency.getInstance("BRL")), bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() } } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index b539dfee7a..c4b0d7473c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -2,7 +2,7 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable import com.google.common.base.Stopwatch -import net.corda.core.contracts.DOLLARS +import net.corda.finance.DOLLARS import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.internal.concurrent.transpose diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index e9db0c5a85..7c94d3b2ca 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -1,7 +1,7 @@ package net.corda.node.services import net.corda.core.contracts.Amount -import net.corda.core.contracts.POUNDS +import net.corda.finance.POUNDS import net.corda.core.identity.Party import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.messaging.CordaRPCOps diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 6d2d235610..316c661234 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -14,6 +14,9 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow import net.corda.core.node.services.queryBy import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.DOLLARS +import net.corda.finance.GBP +import net.corda.finance.USD import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.node.internal.CordaRPCOpsImpl diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 8f415beb52..2e32b19b04 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -33,6 +33,8 @@ import net.corda.core.utilities.days import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.unwrap +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` import net.corda.flows.TwoPartyTradeFlow.Buyer import net.corda.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.AbstractNode diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 923457f44b..fc7bd1081e 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -10,6 +10,9 @@ import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction +import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS +import net.corda.finance.SWISS_FRANCS import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.VaultSchemaV1 diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt index 3f1f0e909c..cb57469f9e 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt @@ -4,7 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.Issued -import net.corda.core.contracts.USD +import net.corda.finance.USD import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 41118d6f83..8eed891813 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -5,7 +5,7 @@ import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.concurrent.Semaphore import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.ContractState -import net.corda.core.contracts.DOLLARS +import net.corda.finance.DOLLARS import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue 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 418c62f631..d2e7dc3980 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 @@ -20,6 +20,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet +import net.corda.finance.* import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 47ece34c00..b0c5e25802 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -15,6 +15,7 @@ import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* import net.corda.core.utilities.* +import net.corda.finance.* import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.schemas.CashSchemaV1 diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 0b35c7a775..db0d369623 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -4,7 +4,9 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.contracts.getCashBalance -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryService @@ -13,6 +15,7 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.* import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.* diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 5baaa550e1..bfeeb8691f 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -1,9 +1,8 @@ package net.corda.bank import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS +import net.corda.finance.DOLLARS import net.corda.core.messaging.startFlow -import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.QueryCriteria diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index a2be1feb62..c5e6e96263 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -3,15 +3,14 @@ package net.corda.bank.api import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Amount -import net.corda.core.contracts.currency import net.corda.core.messaging.startFlow -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.testing.DUMMY_NOTARY import net.corda.testing.http.HttpApi +import java.util.* /** * Interface for communicating with Bank of Corda node @@ -46,7 +45,7 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity) ?: throw IllegalStateException("Unable to locate notary node in network map cache") - val amount = Amount(params.amount, currency(params.currency)) + val amount = Amount(params.amount, Currency.getInstance(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) return rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryNode.notaryIdentity, params.anonymous) diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 47e7c2bacc..5524c4ba30 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -1,7 +1,6 @@ package net.corda.bank.api import net.corda.core.contracts.Amount -import net.corda.core.contracts.currency import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes @@ -10,6 +9,7 @@ import net.corda.core.utilities.loggerFor import net.corda.flows.IssuerFlow.IssuanceRequester import org.bouncycastle.asn1.x500.X500Name import java.time.LocalDateTime +import java.util.* import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response @@ -49,9 +49,9 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { val notaryParty = rpc.partyFromX500Name(params.notaryName) ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.notaryName} in identity service").build() val notaryNode = rpc.nodeIdentityFromParty(notaryParty) - ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${notaryParty} in network map service").build() + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate $notaryParty in network map service").build() - val amount = Amount(params.amount, currency(params.currency)) + val amount = Amount(params.amount, Currency.getInstance(params.currency)) val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) val anonymous = params.anonymous @@ -62,7 +62,7 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { logger.info("Issue request completed successfully: $params") Response.status(Response.Status.CREATED).build() } catch (e: Exception) { - logger.error("Issue request failed: ${e}", e) + logger.error("Issue request failed", e) Response.status(Response.Status.FORBIDDEN).build() } } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index c501759e09..9bed90aa82 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -14,6 +14,7 @@ import net.corda.core.node.services.ServiceInfo 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.irs.flows.RatesFixFlow import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 68c4bb195b..05f4532d02 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -1,10 +1,13 @@ package net.corda.irs.contract import net.corda.contracts.* -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds +import net.corda.finance.DOLLARS +import net.corda.finance.EUR import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Test @@ -79,8 +82,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { fixedLegPaymentSchedule = mutableMapOf() ) - val EUR = currency("EUR") - val common = InterestRateSwap.Common( baseCurrency = EUR, eligibleCurrency = EUR, @@ -169,8 +170,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { fixedLegPaymentSchedule = mutableMapOf() ) - val EUR = currency("EUR") - val common = InterestRateSwap.Common( baseCurrency = EUR, eligibleCurrency = EUR, diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index 4db198241d..6a57156a52 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -11,6 +11,7 @@ import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.GBP @StartableByRPC class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: Party, private val discriminator: Int) : FlowLogic() { diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index 4955c54207..af2a0b034c 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -1,20 +1,19 @@ package net.corda.traderdemo import net.corda.client.rpc.CordaRPCClient -import net.corda.core.contracts.DOLLARS -import net.corda.core.utilities.millis -import net.corda.core.node.services.ServiceInfo import net.corda.core.internal.concurrent.transpose +import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.millis +import net.corda.finance.DOLLARS import net.corda.flows.CashIssueFlow -import net.corda.testing.DUMMY_BANK_A -import net.corda.testing.DUMMY_BANK_B -import net.corda.testing.DUMMY_NOTARY -import net.corda.flows.IssuerFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.poll import net.corda.testing.node.NodeBasedTest import net.corda.traderdemo.flow.BuyerFlow @@ -30,7 +29,7 @@ class TraderDemoTest : NodeBasedTest() { val demoUser = User("demo", "demo", setOf(startFlowPermission())) val bankUser = User("user1", "test", permissions = setOf(startFlowPermission(), startFlowPermission())) - val (nodeA, nodeB, bankNode, notaryNode) = listOf( + val (nodeA, nodeB, bankNode) = listOf( startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)), startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)), startNode(BOC.name, rpcUsers = listOf(bankUser)), @@ -57,7 +56,7 @@ class TraderDemoTest : NodeBasedTest() { val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount) // TODO: Enable anonymisation - clientBank.runIssuer(amount = 100.DOLLARS, buyerName = nodeA.info.legalIdentity.name, sellerName = nodeB.info.legalIdentity.name, notaryName = notaryNode.info.legalIdentity.name) + clientBank.runIssuer(amount = 100.DOLLARS, buyerName = nodeA.info.legalIdentity.name, sellerName = nodeB.info.legalIdentity.name) clientB.runSeller(buyerName = nodeA.info.legalIdentity.name, amount = 5.DOLLARS) assertThat(clientA.cashCount).isGreaterThan(originalACash) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt index f7accdc4ae..689d3b4e52 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemo.kt @@ -2,12 +2,11 @@ package net.corda.traderdemo import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient -import net.corda.core.contracts.DOLLARS import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor +import net.corda.finance.DOLLARS import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B -import net.corda.testing.DUMMY_NOTARY import org.slf4j.Logger import kotlin.system.exitProcess @@ -28,8 +27,6 @@ private class TraderDemo { val logger: Logger = loggerFor() val buyerName = DUMMY_BANK_A.name val sellerName = DUMMY_BANK_B.name - val notaryName = DUMMY_NOTARY.name - val buyerRpcPort = 10006 val sellerRpcPort = 10009 val bankRpcPort = 10012 } @@ -52,7 +49,7 @@ private class TraderDemo { if (role == Role.BANK) { val bankHost = NetworkHostAndPort("localhost", bankRpcPort) CordaRPCClient(bankHost).use("demo", "demo") { - TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName, notaryName) + TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName) } } else { val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index e8e5d0d924..101224dde2 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -4,8 +4,6 @@ import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.Cash import net.corda.contracts.getCashBalance import net.corda.core.contracts.Amount -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.USD import net.corda.core.internal.Emoji import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.CordaRPCOps @@ -15,7 +13,8 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.builder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.loggerFor +import net.corda.finance.DOLLARS +import net.corda.finance.USD import net.corda.flows.CashIssueFlow import net.corda.node.services.vault.VaultSchemaV1 import net.corda.testing.DUMMY_NOTARY @@ -29,10 +28,6 @@ import java.util.* * Interface for communicating with nodes running the trader demo. */ class TraderDemoClientApi(val rpc: CordaRPCOps) { - private companion object { - val logger = loggerFor() - } - val cashCount: Long get() { val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) @@ -47,7 +42,7 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { return rpc.vaultQueryBy(countCriteria).otherResults.single() as Long } - fun runIssuer(amount: Amount = 1100.0.DOLLARS, buyerName: X500Name, sellerName: X500Name, notaryName: X500Name) { + fun runIssuer(amount: Amount, buyerName: X500Name, sellerName: X500Name) { val ref = OpaqueBytes.of(1) val buyer = rpc.partyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName") val seller = rpc.partyFromX500Name(sellerName) ?: throw IllegalStateException("Don't know $sellerName") @@ -77,7 +72,7 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { } // The line below blocks and waits for the future to resolve. - val stx = rpc.startFlow(::CommercialPaperIssueFlow, amount, ref, seller, notaryNode.notaryIdentity).returnValue.getOrThrow() + rpc.startFlow(::CommercialPaperIssueFlow, amount, ref, seller, notaryNode.notaryIdentity).returnValue.getOrThrow() println("Commercial paper issued to seller") } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt index 56a4b4696d..2df13766d3 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt @@ -2,16 +2,14 @@ package net.corda.traderdemo.flow import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper -import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.Amount -import net.corda.core.contracts.`issued by` +import net.corda.finance.`issued by` import net.corda.core.crypto.SecureHash import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party -import net.corda.core.node.NodeInfo import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 4b17207b51..54f19f966a 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -8,8 +8,8 @@ import net.corda.client.mock.pickOne import net.corda.client.rpc.CordaRPCConnection import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.GBP -import net.corda.core.contracts.USD +import net.corda.finance.GBP +import net.corda.finance.USD import net.corda.core.identity.Party import net.corda.core.internal.concurrent.thenMatch import net.corda.core.messaging.CordaRPCOps diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt index 31dafe684e..9acf2aab13 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt @@ -6,9 +6,9 @@ import net.corda.client.jfx.model.observableList import net.corda.client.jfx.model.observableValue import net.corda.client.jfx.utils.ChosenList import net.corda.client.jfx.utils.map -import net.corda.core.contracts.currency import net.corda.core.node.NodeInfo import tornadofx.* +import java.util.* val ISSUER_SERVICE_TYPE = Regex("corda.issuer.(USD|GBP|CHF|EUR)") @@ -34,7 +34,7 @@ class IssuerModel { private fun NodeInfo.issuerCurrency() = if (isIssuerNode()) { val issuer = advertisedServices.first { it.info.type.id.matches(ISSUER_SERVICE_TYPE) } - currency(issuer.info.type.id.substringAfterLast(".")) + Currency.getInstance(issuer.info.type.id.substringAfterLast(".")) } else null } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt index 3e1bf5a917..9c3e413382 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt @@ -6,6 +6,10 @@ import net.corda.client.jfx.model.ExchangeRateModel import net.corda.client.jfx.model.observableValue import net.corda.client.jfx.utils.AmountBindings import net.corda.core.contracts.* +import net.corda.finance.CHF +import net.corda.finance.EUR +import net.corda.finance.GBP +import net.corda.finance.USD import org.fxmisc.easybind.EasyBind import tornadofx.* import java.util.* diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt index 2e85b434d8..5df84b94fd 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/SettingsModel.kt @@ -4,7 +4,6 @@ import javafx.beans.InvalidationListener import javafx.beans.Observable import javafx.beans.property.ObjectProperty import javafx.beans.property.SimpleObjectProperty -import net.corda.core.contracts.currency import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists @@ -59,7 +58,7 @@ class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable { String::class.java -> string(metadata.name, "") as T Int::class.java -> string(metadata.name, "0").toInt() as T Boolean::class.java -> boolean(metadata.name) as T - Currency::class.java -> currency(string(metadata.name, "USD")) as T + Currency::class.java -> Currency.getInstance(string(metadata.name, "USD")) as T Path::class.java -> Paths.get(string(metadata.name, "")).toAbsolutePath() as T else -> throw IllegalArgumentException("Unsupported type ${metadata.returnType}") } diff --git a/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt b/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt index 30a185aa67..a363e8cd3f 100644 --- a/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt +++ b/tools/explorer/src/test/kotlin/net/corda/explorer/model/IssuerModelTest.kt @@ -1,23 +1,27 @@ package net.corda.explorer.model -import net.corda.core.contracts.USD -import net.corda.core.contracts.currency +import net.corda.finance.USD import org.junit.Test +import java.util.* +import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue class IssuerModelTest { - @Test fun `test issuer regex`() { val regex = Regex("corda.issuer.(USD|GBP|CHF)") - kotlin.test.assertTrue("corda.issuer.USD".matches(regex)) - kotlin.test.assertTrue("corda.issuer.GBP".matches(regex)) + assertTrue("corda.issuer.USD".matches(regex)) + assertTrue("corda.issuer.GBP".matches(regex)) - kotlin.test.assertFalse("corda.issuer.USD.GBP".matches(regex)) - kotlin.test.assertFalse("corda.issuer.EUR".matches(regex)) - kotlin.test.assertFalse("corda.issuer".matches(regex)) + assertFalse("corda.issuer.USD.GBP".matches(regex)) + assertFalse("corda.issuer.EUR".matches(regex)) + assertFalse("corda.issuer".matches(regex)) - kotlin.test.assertEquals(USD, currency("corda.issuer.USD".substringAfterLast("."))) - assertFailsWith(IllegalArgumentException::class, { currency("corda.issuer.DOLLAR".substringBeforeLast(".")) }) + assertEquals(USD, Currency.getInstance("corda.issuer.USD".substringAfterLast("."))) + assertFailsWith(IllegalArgumentException::class) { + Currency.getInstance("corda.issuer.DOLLAR".substringBeforeLast(".")) + } } } \ No newline at end of file diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt index f8b8ae4a61..f9a980f992 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt @@ -5,7 +5,7 @@ import net.corda.client.mock.pickN import net.corda.contracts.asset.Cash import net.corda.core.contracts.Issued import net.corda.core.contracts.PartyAndReference -import net.corda.core.contracts.USD +import net.corda.finance.USD import net.corda.core.identity.AbstractParty import net.corda.core.internal.concurrent.thenMatch import net.corda.core.messaging.vaultQueryBy diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt index 2402179a7b..d9a50b4b06 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt @@ -5,7 +5,7 @@ import net.corda.client.mock.Generator import net.corda.client.mock.pickOne import net.corda.client.mock.replicatePoisson import net.corda.contracts.asset.Cash -import net.corda.core.contracts.USD +import net.corda.finance.USD import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import net.corda.core.utilities.getOrThrow diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt index 26d8fe66dd..a8ad5777f1 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt @@ -2,7 +2,7 @@ package net.corda.loadtest.tests import net.corda.client.mock.Generator import net.corda.core.contracts.Amount -import net.corda.core.contracts.USD +import net.corda.finance.USD import net.corda.core.flows.FlowException import net.corda.core.internal.concurrent.thenMatch import net.corda.core.utilities.OpaqueBytes diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 084e0140e4..69e3f42738 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -1,7 +1,7 @@ package net.corda.verifier import net.corda.client.mock.generateOrFail -import net.corda.core.contracts.DOLLARS +import net.corda.finance.DOLLARS import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.OpaqueBytes From 3138e2b6dec0189a69eaf5237fc63ec971b860cb Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 15 Aug 2017 15:42:01 +0100 Subject: [PATCH 005/101] Cleaned up the flow stack snapshot API --- .../kotlin/net/corda/core/flows/FlowLogic.kt | 6 +- .../net/corda/core/flows/FlowStackSnapshot.kt | 68 +++---------------- .../corda/core/internal/FlowStateMachine.kt | 23 +------ .../statemachine/FlowStackSnapshotFactory.kt | 40 +++++++++++ .../statemachine/FlowStateMachineImpl.kt | 10 ++- .../net/corda/node/InteractiveShellTest.kt | 26 ++----- .../corda/testing/FlowStackSnapshotTest.kt | 63 +++++++++-------- .../net/corda/testing/FlowStackSnapshot.kt | 66 +++++++++--------- ...ces.statemachine.FlowStackSnapshotFactory} | 0 9 files changed, 127 insertions(+), 175 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/services/statemachine/FlowStackSnapshotFactory.kt rename test-utils/src/main/resources/META-INF/services/{net.corda.core.flows.FlowStackSnapshotFactory => net.corda.node.services.statemachine.FlowStackSnapshotFactory} (100%) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 6a8d7f68f9..d6752b4383 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -140,7 +140,7 @@ abstract class FlowLogic { * network's event horizon time. */ @Suspendable - open fun send(otherParty: Party, payload: Any): Unit = stateMachine.send(otherParty, payload, flowUsedForSessions) + open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions) /** * Invokes the given subflow. This function returns once the subflow completes successfully with the result @@ -239,7 +239,7 @@ abstract class FlowLogic { * Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect * what objects would be serialised at the time of call to a suspending action (e.g. send/receive). * Note: This logic is only available during tests and is not meant to be used during the production deployment. - * Therefore the default implementationdoes nothing. + * Therefore the default implementation does nothing. */ @Suspendable fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java) @@ -256,7 +256,7 @@ abstract class FlowLogic { * Therefore the default implementation does nothing. */ @Suspendable - fun persistFlowStackSnapshot(): Unit = stateMachine.persistFlowStackSnapshot(this::class.java) + fun persistFlowStackSnapshot() = stateMachine.persistFlowStackSnapshot(this::class.java) //////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt b/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt index c1866adcc8..5730a968a1 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt @@ -1,69 +1,21 @@ package net.corda.core.flows -import net.corda.core.utilities.loggerFor -import java.nio.file.Path -import java.util.* - -interface FlowStackSnapshotFactory { - private object Holder { - val INSTANCE: FlowStackSnapshotFactory - - init { - val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull() - INSTANCE = serviceFactory ?: FlowStackSnapshotDefaultFactory() - } - } - - companion object { - val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE } - } - - /** - * Returns flow stack data snapshot extracted from Quasar stack. - * It is designed to be used in the debug mode of the flow execution. - * Note. This logic is only available during tests and is not meant to be used during the production deployment. - * Therefore the default implementation does nothing. - */ - fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? - - /** Stores flow stack snapshot as a json file. The stored shapshot is only partial and consists - * only data (i.e. stack traces and local variables values) relevant to the flow. It does not - * persist corda internal data (e.g. FlowStateMachine). Instead it uses [StackFrameDataToken] to indicate - * the class of the element on the stack. - * The flow stack snapshot is stored in a file located in - * {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/ - * where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform. - * Note. This logic is only available during tests and is not meant to be used during the production deployment. - * Therefore the default implementation does nothing. - */ - fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String): Unit -} - -private class FlowStackSnapshotDefaultFactory : FlowStackSnapshotFactory { - val log = loggerFor() - - override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? { - log.warn("Flow stack snapshot are not supposed to be used in a production deployment") - return null - } - - override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) { - log.warn("Flow stack snapshot are not supposed to be used in a production deployment") - } -} +import java.time.Instant /** * Main data object representing snapshot of the flow stack, extracted from the Quasar stack. */ -data class FlowStackSnapshot constructor( - val timestamp: Long = System.currentTimeMillis(), - val flowClass: Class<*>? = null, - val stackFrames: List = listOf() +data class FlowStackSnapshot( + val time: Instant, + val flowClass: Class>, + val stackFrames: List ) { data class Frame( - val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects] - val stackObjects: List = listOf() - ) + val stackTraceElement: StackTraceElement, // This should be the call that *pushed* the frame of [objects] + val stackObjects: List + ) { + override fun toString(): String = stackTraceElement.toString() + } } /** diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index 7ee183b348..c855b8ea70 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -3,11 +3,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowContext -import net.corda.core.flows.FlowInitiator -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStackSnapshot -import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction @@ -39,24 +35,11 @@ interface FlowStateMachine { fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map): Unit - /** - * Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect - * what objects would be serialised at the time of call to a suspending action (e.g. send/receive). - */ @Suspendable - fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? + fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? - /** - * Persists a shallow copy of the Quasar stack frames at the time of call to [persistFlowStackSnapshot]. - * Use this to track the monitor evolution of the quasar stack values during the flow execution. - * The flow stack snapshot is stored in a file located in {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/ - * where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform. - * - * Note: With respect to the [flowStackSnapshot], the snapshot being persisted by this method is partial, - * meaning that only flow relevant traces and local variables are persisted. - */ @Suspendable - fun persistFlowStackSnapshot(flowClass: Class<*>): Unit + fun persistFlowStackSnapshot(flowClass: Class>): Unit val serviceHub: ServiceHub val logger: Logger diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStackSnapshotFactory.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStackSnapshotFactory.kt new file mode 100644 index 0000000000..72a7926eb2 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStackSnapshotFactory.kt @@ -0,0 +1,40 @@ +package net.corda.node.services.statemachine + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowStackSnapshot +import net.corda.core.flows.StateMachineRunId +import net.corda.core.utilities.loggerFor +import java.nio.file.Path +import java.util.* + +interface FlowStackSnapshotFactory { + private object Holder { + val INSTANCE: FlowStackSnapshotFactory + + init { + val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull() + INSTANCE = serviceFactory ?: DefaultFlowStackSnapshotFactory + } + } + + companion object { + val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE } + } + + fun getFlowStackSnapshot(flowClass: Class>): FlowStackSnapshot? + + fun persistAsJsonFile(flowClass: Class>, baseDir: Path, flowId: StateMachineRunId) + + private object DefaultFlowStackSnapshotFactory : FlowStackSnapshotFactory { + private val log = loggerFor() + + override fun getFlowStackSnapshot(flowClass: Class>): FlowStackSnapshot? { + log.warn("Flow stack snapshot are not supposed to be used in a production deployment") + return null + } + + override fun persistAsJsonFile(flowClass: Class>, baseDir: Path, flowId: StateMachineRunId) { + log.warn("Flow stack snapshot are not supposed to be used in a production deployment") + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 3820c0976f..977e1b416e 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -255,14 +255,12 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } @Suspendable - override fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? { - val factory = FlowStackSnapshotFactory.instance - return factory.getFlowStackSnapshot(flowClass) + override fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? { + return FlowStackSnapshotFactory.instance.getFlowStackSnapshot(flowClass) } - override fun persistFlowStackSnapshot(flowClass: Class<*>): Unit { - val factory = FlowStackSnapshotFactory.instance - factory.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id.toString()) + override fun persistFlowStackSnapshot(flowClass: Class>) { + FlowStackSnapshotFactory.instance.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id) } /** diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index 4838ee78b5..6a70ea30a0 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -4,11 +4,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Amount import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowContext -import net.corda.core.flows.FlowInitiator -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStackSnapshot -import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.node.ServiceHub @@ -93,21 +89,9 @@ class InteractiveShellTest { override val id: StateMachineRunId get() = throw UnsupportedOperationException() override val resultFuture: CordaFuture get() = throw UnsupportedOperationException() override val flowInitiator: FlowInitiator get() = throw UnsupportedOperationException() - - override fun checkFlowPermission(permissionName: String, extraAuditData: Map) { - // Do nothing - } - - override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) { - // Do nothing - } - - override fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? { - return null - } - - override fun persistFlowStackSnapshot(flowClass: Class<*>) { - // Do nothing - } + override fun checkFlowPermission(permissionName: String, extraAuditData: Map) = Unit + override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) = Unit + override fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? = null + override fun persistFlowStackSnapshot(flowClass: Class>) = Unit } } \ No newline at end of file diff --git a/test-utils/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt b/test-utils/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt index 613ee0b0ca..6625b3fa35 100644 --- a/test-utils/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt +++ b/test-utils/src/integration-test/kotlin/net/corda/testing/FlowStackSnapshotTest.kt @@ -1,21 +1,23 @@ package net.corda.testing import co.paralleluniverse.fibers.Suspendable -import com.fasterxml.jackson.databind.ObjectMapper import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowStackSnapshot import net.corda.core.flows.StartableByRPC +import net.corda.core.flows.StateMachineRunId +import net.corda.core.internal.div +import net.corda.core.internal.list +import net.corda.core.internal.read import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable +import net.corda.jackson.JacksonSupport import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.driver.driver import org.junit.Ignore import org.junit.Test -import java.io.File import java.nio.file.Path -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter +import java.time.LocalDate import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -110,15 +112,15 @@ object Constants { * No side effect flow that stores the partial snapshot into a file, path to which is passed in the flow constructor. */ @StartableByRPC -class PersistingNoSideEffectFlow : FlowLogic() { +class PersistingNoSideEffectFlow : FlowLogic() { @Suspendable - override fun call(): String { + override fun call(): StateMachineRunId { // Using the [Constants] object here is considered by Quasar as a side effect. Thus explicit initialization @Suppress("UNUSED_VARIABLE") val unusedVar = "inCall" persist() - return stateMachine.id.toString() + return stateMachine.id } @Suspendable @@ -134,14 +136,14 @@ class PersistingNoSideEffectFlow : FlowLogic() { * Flow with side effects that stores the partial snapshot into a file, path to which is passed in the flow constructor. */ @StartableByRPC -class PersistingSideEffectFlow : FlowLogic() { +class PersistingSideEffectFlow : FlowLogic() { @Suspendable - override fun call(): String { + override fun call(): StateMachineRunId { @Suppress("UNUSED_VARIABLE") val unusedVar = Constants.IN_CALL_VALUE persist() - return stateMachine.id.toString() + return stateMachine.id } @Suspendable @@ -156,16 +158,16 @@ class PersistingSideEffectFlow : FlowLogic() { * Similar to [PersistingSideEffectFlow] but aims to produce multiple snapshot files. */ @StartableByRPC -class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic() { +class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic() { @Suspendable - override fun call(): String { + override fun call(): StateMachineRunId { @Suppress("UNUSED_VARIABLE") val unusedVar = Constants.IN_CALL_VALUE for (i in 1..persistCallCount) { persist() } - return stateMachine.id.toString() + return stateMachine.id } @Suspendable @@ -176,14 +178,19 @@ class MultiplePersistingSideEffectFlow(val persistCallCount: Int) : FlowLogic())))).get() @@ -211,8 +218,6 @@ class FlowStackSnapshotTest { } @Test - @Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " + - "This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)") fun `flowStackSnapshot contains empty frames when methods with no side effects are called`() { driver(startNodesInProcess = true) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission())))).get() @@ -228,8 +233,6 @@ class FlowStackSnapshotTest { } @Test - @Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " + - "This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)") fun `persistFlowStackSnapshot persists empty frames to a file when methods with no side effects are called`() { driver(startNodesInProcess = true) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission())))).get() @@ -247,8 +250,6 @@ class FlowStackSnapshotTest { } @Test - @Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " + - "This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)") fun `persistFlowStackSnapshot persists multiple snapshots in different files`() { driver(startNodesInProcess = true) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission())))).get() @@ -263,8 +264,6 @@ class FlowStackSnapshotTest { } @Test - @Ignore("This test is skipped due to Jacoco agent interference with the quasar instrumentation process. " + - "This violates tested criteria (specifically extra objects are introduced to the quasar stack by th Jacoco agent)") fun `persistFlowStackSnapshot stack traces are aligned with stack objects`() { driver(startNodesInProcess = true) { val a = startNode(rpcUsers = listOf(User(Constants.USER, Constants.PASSWORD, setOf(startFlowPermission())))).get() @@ -279,11 +278,11 @@ class FlowStackSnapshotTest { it.stackObjects.forEach { when (it) { Constants.IN_CALL_VALUE -> { - assertEquals(PersistingSideEffectFlow::call.name, trace!!.methodName) + assertEquals(PersistingSideEffectFlow::call.name, trace.methodName) inCallCount++ } Constants.IN_PERSIST_VALUE -> { - assertEquals(PersistingSideEffectFlow::persist.name, trace!!.methodName) + assertEquals(PersistingSideEffectFlow::persist.name, trace.methodName) inPersistCount++ } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt b/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt index 13ec465284..d7c6728687 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt @@ -5,23 +5,25 @@ import co.paralleluniverse.fibers.Instrumented import co.paralleluniverse.fibers.Stack import co.paralleluniverse.fibers.Suspendable import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowStackSnapshot import net.corda.core.flows.FlowStackSnapshot.Frame -import net.corda.core.flows.FlowStackSnapshotFactory import net.corda.core.flows.StackFrameDataToken +import net.corda.core.flows.StateMachineRunId import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.div +import net.corda.core.internal.write import net.corda.core.serialization.SerializeAsToken -import java.io.File +import net.corda.jackson.JacksonSupport +import net.corda.node.services.statemachine.FlowStackSnapshotFactory import java.nio.file.Path -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter +import java.time.Instant +import java.time.LocalDate class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { @Suspendable - override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? { + override fun getFlowStackSnapshot(flowClass: Class>): FlowStackSnapshot { var snapshot: FlowStackSnapshot? = null val stackTrace = Fiber.currentFiber().stackTrace Fiber.parkAndSerialize { fiber, _ -> @@ -35,19 +37,19 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { return temporarySnapshot!! } - override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) { + override fun persistAsJsonFile(flowClass: Class>, baseDir: Path, flowId: StateMachineRunId) { val flowStackSnapshot = getFlowStackSnapshot(flowClass) - val mapper = ObjectMapper() - mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) - mapper.enable(SerializationFeature.INDENT_OUTPUT) - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) + val mapper = JacksonSupport.createNonRpcMapper().apply { + disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + setSerializationInclusion(JsonInclude.Include.NON_NULL) + } val file = createFile(baseDir, flowId) - file.bufferedWriter().use { out -> - mapper.writeValue(out, filterOutStackDump(flowStackSnapshot!!)) + file.write(createDirs = true) { + mapper.writeValue(it, filterOutStackDump(flowStackSnapshot)) } } - private fun extractStackSnapshotFromFiber(fiber: Fiber<*>, stackTrace: List, flowClass: Class<*>): FlowStackSnapshot { + private fun extractStackSnapshotFromFiber(fiber: Fiber<*>, stackTrace: List, flowClass: Class>): FlowStackSnapshot { val stack = getFiberStack(fiber) val objectStack = getObjectStack(stack).toList() val frameOffsets = getFrameOffsets(stack) @@ -58,24 +60,25 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { val relevantStackTrace = removeConstructorStackTraceElements(stackTrace).drop(1) val stackTraceToAnnotation = relevantStackTrace.map { val element = StackTraceElement(it.className, it.methodName, it.fileName, it.lineNumber) - element to getInstrumentedAnnotation(element) + element to element.instrumentedAnnotation } val frameObjectsIterator = frameObjects.listIterator() val frames = stackTraceToAnnotation.reversed().map { (element, annotation) -> // If annotation is null then the case indicates that this is an entry point - i.e. // the net.corda.node.services.statemachine.FlowStateMachineImpl.run method - if (frameObjectsIterator.hasNext() && (annotation == null || !annotation.methodOptimized)) { - Frame(element, frameObjectsIterator.next()) + val stackObjects = if (frameObjectsIterator.hasNext() && (annotation == null || !annotation.methodOptimized)) { + frameObjectsIterator.next() } else { - Frame(element, listOf()) + emptyList() } + Frame(element, stackObjects) } - return FlowStackSnapshot(flowClass = flowClass, stackFrames = frames) + return FlowStackSnapshot(Instant.now(), flowClass, frames) } - private fun getInstrumentedAnnotation(element: StackTraceElement): Instrumented? { - Class.forName(element.className).methods.forEach { - if (it.name == element.methodName && it.isAnnotationPresent(Instrumented::class.java)) { + private val StackTraceElement.instrumentedAnnotation: Instrumented? get() { + Class.forName(className).methods.forEach { + if (it.name == methodName && it.isAnnotationPresent(Instrumented::class.java)) { return it.getAnnotation(Instrumented::class.java) } } @@ -99,10 +102,10 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { private fun filterOutStackDump(flowStackSnapshot: FlowStackSnapshot): FlowStackSnapshot { val framesFilteredByStackTraceElement = flowStackSnapshot.stackFrames.filter { - !FlowStateMachine::class.java.isAssignableFrom(Class.forName(it.stackTraceElement!!.className)) + !FlowStateMachine::class.java.isAssignableFrom(Class.forName(it.stackTraceElement.className)) } val framesFilteredByObjects = framesFilteredByStackTraceElement.map { - Frame(it.stackTraceElement, it.stackObjects.map { + it.copy(stackObjects = it.stackObjects.map { if (it != null && (it is FlowLogic<*> || it is FlowStateMachine<*> || it is Fiber<*> || it is SerializeAsToken)) { StackFrameDataToken(it::class.java.name) } else { @@ -110,25 +113,18 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { } }) } - return FlowStackSnapshot(flowStackSnapshot.timestamp, flowStackSnapshot.flowClass, framesFilteredByObjects) + return flowStackSnapshot.copy(stackFrames = framesFilteredByObjects) } - private fun createFile(baseDir: Path, flowId: String): File { - val file: File - val dir = File(baseDir.toFile(), "flowStackSnapshots/${LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)}/$flowId/") + private fun createFile(baseDir: Path, flowId: StateMachineRunId): Path { + val dir = baseDir / "flowStackSnapshots" / LocalDate.now().toString() / flowId.uuid.toString() val index = ThreadLocalIndex.currentIndex.get() - if (index == 0) { - dir.mkdirs() - file = File(dir, "flowStackSnapshot.json") - } else { - file = File(dir, "flowStackSnapshot-$index.json") - } + val file = if (index == 0) dir / "flowStackSnapshot.json" else dir / "flowStackSnapshot-$index.json" ThreadLocalIndex.currentIndex.set(index + 1) return file } private class ThreadLocalIndex private constructor() { - companion object { val currentIndex = object : ThreadLocal() { override fun initialValue() = 0 diff --git a/test-utils/src/main/resources/META-INF/services/net.corda.core.flows.FlowStackSnapshotFactory b/test-utils/src/main/resources/META-INF/services/net.corda.node.services.statemachine.FlowStackSnapshotFactory similarity index 100% rename from test-utils/src/main/resources/META-INF/services/net.corda.core.flows.FlowStackSnapshotFactory rename to test-utils/src/main/resources/META-INF/services/net.corda.node.services.statemachine.FlowStackSnapshotFactory From 388863505575f454572d1037f3140ada0ec266a8 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 14 Aug 2017 16:24:49 +0100 Subject: [PATCH 006/101] Removed redundant serialisation of checkpoints (#1004) --- .../persistence/DBCheckpointStorage.kt | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index b977f36875..a82642adf7 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -1,11 +1,10 @@ package net.corda.node.services.persistence -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize +import net.corda.core.serialization.SerializedBytes import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage -import net.corda.node.utilities.* +import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.NODE_DATABASE_PREFIX import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id @@ -28,11 +27,11 @@ class DBCheckpointStorage : CheckpointStorage { var checkpoint: ByteArray = ByteArray(0) ) - override fun addCheckpoint(value: Checkpoint) { + override fun addCheckpoint(checkpoint: Checkpoint) { val session = DatabaseTransactionManager.current().session session.save(DBCheckpoint().apply { - checkpointId = value.id.toString() - checkpoint = value.serialize(context = SerializationDefaults.CHECKPOINT_CONTEXT).bytes + checkpointId = checkpoint.id.toString() + this.checkpoint = checkpoint.serializedFiber.bytes }) } @@ -50,10 +49,9 @@ class DBCheckpointStorage : CheckpointStorage { val criteriaQuery = session.criteriaBuilder.createQuery(DBCheckpoint::class.java) val root = criteriaQuery.from(DBCheckpoint::class.java) criteriaQuery.select(root) - val query = session.createQuery(criteriaQuery) - val checkpoints = query.resultList.map { e -> e.checkpoint.deserialize(context = SerializationDefaults.CHECKPOINT_CONTEXT) }.asSequence() - for (e in checkpoints) { - if (!block(e)) { + for (row in session.createQuery(criteriaQuery).resultList) { + val checkpoint = Checkpoint(SerializedBytes(row.checkpoint)) + if (!block(checkpoint)) { break } } From 89476904fce9d8852df77be65d57bfebb6d99cd5 Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 11 Aug 2017 14:03:23 +0100 Subject: [PATCH 007/101] Remove IssuerFlow * Remove IssuerFlow as it is dangerous and its presence in the finance module risks accidental use in non-test code. As written it will issue arbitary amounts of currency on request from any node on the network, with no validation barring that the currency type is valid. * Unify interface to CashIssueFlow to match the previous IssuerFlow --- .../corda/client/jfx/NodeMonitorModelTest.kt | 6 +- .../corda/client/rpc/CordaRPCClientTest.kt | 18 +- .../net/corda/core/messaging/CordaRPCOps.kt | 23 +++ .../core/flows/ContractUpgradeFlowTest.kt | 2 +- docs/source/changelog.rst | 3 + .../corda/docs/IntegrationTestingTutorial.kt | 6 +- .../net/corda/docs/ClientRpcTutorial.kt | 2 +- .../kotlin/net/corda/docs/CustomVaultQuery.kt | 3 +- .../net/corda/docs/CustomVaultQueryTest.kt | 5 +- .../docs/FxTransactionBuildTutorialTest.kt | 6 +- docs/source/release-notes.rst | 2 + .../kotlin/net/corda/flows/CashFlowCommand.kt | 2 +- .../kotlin/net/corda/flows/CashIssueFlow.kt | 30 ++-- .../main/kotlin/net/corda/flows/IssuerFlow.kt | 123 ------------- .../net/corda/flows/CashExitFlowTests.kt | 6 +- .../net/corda/flows/CashIssueFlowTests.kt | 15 +- .../net/corda/flows/CashPaymentFlowTests.kt | 7 +- .../kotlin/net/corda/flows/IssuerFlowTest.kt | 167 ------------------ .../net/corda/node/internal/AbstractNode.kt | 4 - .../net/corda/node/CordaRPCOpsImplTest.kt | 8 +- .../statemachine/FlowFrameworkTests.kt | 3 +- samples/bank-of-corda-demo/build.gradle | 2 +- .../corda/bank/BankOfCordaRPCClientTest.kt | 12 +- .../net/corda/bank/BankOfCordaDriver.kt | 4 +- .../corda/bank/api/BankOfCordaClientApi.kt | 6 +- .../net/corda/bank/api/BankOfCordaWebApi.kt | 8 +- .../corda/traderdemo/TraderDemoClientApi.kt | 3 +- .../test/kotlin/net/corda/traderdemo/Main.kt | 15 +- .../views/cordapps/cash/NewTransaction.kt | 2 +- 29 files changed, 130 insertions(+), 363 deletions(-) delete mode 100644 finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt delete mode 100644 finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 797d857bd6..3d98864cb4 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -111,8 +111,9 @@ class NodeMonitorModelTest : DriverBasedTest() { val anonymous = false rpc.startFlow(::CashIssueFlow, Amount(100, USD), - OpaqueBytes(ByteArray(1, { 1 })), aliceNode.legalIdentity, + rpc.nodeIdentity().legalIdentity, + OpaqueBytes(ByteArray(1, { 1 })), notaryNode.notaryIdentity, anonymous ) @@ -136,7 +137,8 @@ class NodeMonitorModelTest : DriverBasedTest() { @Test fun `cash issue and move`() { val anonymous = false - rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() + rpc.startFlow(::CashIssueFlow, 100.DOLLARS, aliceNode.legalIdentity, rpc.nodeIdentity().legalIdentity, OpaqueBytes.of(1), + notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow() var issueSmId: StateMachineRunId? = null diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 8ef546ab54..92febedcd0 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -77,9 +77,10 @@ class CordaRPCClientTest : NodeBasedTest() { login(rpcUser.username, rpcUser.password) println("Creating proxy") println("Starting flow") - val flowHandle = connection!!.proxy.startTrackedFlow( - ::CashIssueFlow, - 20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity) + val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow, + 20.DOLLARS, node.info.legalIdentity, + node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true + ) println("Started flow, waiting on result") flowHandle.progress.subscribe { println("PROGRESS $it") @@ -113,8 +114,8 @@ class CordaRPCClientTest : NodeBasedTest() { assertTrue(startCash.isEmpty(), "Should not start with any cash") val flowHandle = proxy.startFlow(::CashIssueFlow, - 123.DOLLARS, OpaqueBytes.of(0), - node.info.legalIdentity, node.info.legalIdentity + 123.DOLLARS, node.info.legalIdentity, + node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true ) println("Started issuing cash, waiting on result") flowHandle.returnValue.get() @@ -140,10 +141,11 @@ class CordaRPCClientTest : NodeBasedTest() { } } val nodeIdentity = node.info.legalIdentity - node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity, nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow() + node.services.startFlow(CashIssueFlow(2000.DOLLARS, nodeIdentity, nodeIdentity, OpaqueBytes.of(0), nodeIdentity, true), FlowInitiator.Shell).resultFuture.getOrThrow() proxy.startFlow(::CashIssueFlow, - 123.DOLLARS, OpaqueBytes.of(0), - nodeIdentity, nodeIdentity + 123.DOLLARS, nodeIdentity, + nodeIdentity, OpaqueBytes.of(0), nodeIdentity, + true ).returnValue.getOrThrow() proxy.startFlowDynamic(CashIssueFlow::class.java, 1000.DOLLARS, OpaqueBytes.of(0), diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 601e8c6b11..3133e1c3b0 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -421,6 +421,29 @@ inline fun > CordaRPCOps.startTrac arg3: D ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) +@Suppress("unused") +inline fun > CordaRPCOps.startTrackedFlow( + @Suppress("unused_parameter") + flowConstructor: (A, B, C, D, E) -> R, + arg0: A, + arg1: B, + arg2: C, + arg3: D, + arg4: E +): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4) + +@Suppress("unused") +inline fun > CordaRPCOps.startTrackedFlow( + @Suppress("unused_parameter") + flowConstructor: (A, B, C, D, E, F) -> R, + arg0: A, + arg1: B, + arg2: C, + arg3: D, + arg4: E, + arg5: F +): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5) + /** * The Data feed contains a snapshot of the requested data and an [Observable] of future updates. */ diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index d67cd2b010..5925b6c35c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -176,7 +176,7 @@ class ContractUpgradeFlowTest { fun `upgrade Cash to v2`() { // Create some cash. val anonymous = false - val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture + val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), a.info.legalIdentity, a.info.legalIdentity, OpaqueBytes.of(1), notary, anonymous)).resultFuture mockNet.runNetwork() val stx = result.getOrThrow().stx val stateAndRef = stx.tx.outRef(0) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 6eb3c270ef..a5d12f5e78 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -42,6 +42,9 @@ UNRELEASED * Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`. +* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use + `CashIssueFlow` instead. + Milestone 14 ------------ diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index aec0c46e78..6f7f39ceae 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -67,9 +67,11 @@ class IntegrationTestingTutorial { thread { futures.push(aliceProxy.startFlow(::CashIssueFlow, i.DOLLARS, - issueRef, bob.nodeInfo.legalIdentity, - notary.nodeInfo.notaryIdentity + alice.nodeInfo.legalIdentity, + issueRef, + notary.nodeInfo.notaryIdentity, + true ).returnValue) } }.forEach(Thread::join) // Ensure the stack of futures is populated. diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index e5279ef579..4a795d38a3 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -128,7 +128,7 @@ fun generateTransactions(proxy: CordaRPCOps) { proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me) } else { val quantity = Math.abs(random.nextLong() % 1000) - proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, me, notary) + proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), me, me, issueRef, notary, true) ownedQuantity += quantity } } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt index 142c0e2920..63b6e096e7 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt @@ -132,8 +132,9 @@ object TopupIssuerFlow { val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity // invoke Cash subflow to issue Asset progressTracker.currentStep = ISSUING + val issuer = serviceHub.myInfo.legalIdentity val issueRecipient = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false) + val issueCashFlow = CashIssueFlow(amount, issueRecipient, issuer, issuerPartyRef, notaryParty, anonymous = false) val issueTx = subFlow(issueCashFlow) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // short-circuit when issuing to self diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 7fbce2ed2e..87a598eb03 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -65,10 +65,11 @@ class CustomVaultQueryTest { private fun issueCashForCurrency(amountToIssue: Amount) { // Use NodeA as issuer and create some dollars val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(amountToIssue, - OpaqueBytes.of(0x01), nodeA.info.legalIdentity, + nodeA.info.legalIdentity, + OpaqueBytes.of(0x01), notaryNode.info.notaryIdentity, - false)) + anonymous = false)) // Wait for the flow to stop and print flowHandle1.resultFuture.getOrThrow() } diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index b860e43c02..7d0b631e10 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -45,8 +45,9 @@ class FxTransactionBuildTutorialTest { fun `Run ForeignExchangeFlow to completion`() { // Use NodeA as issuer and create some dollars val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000), - OpaqueBytes.of(0x01), nodeA.info.legalIdentity, + nodeA.info.legalIdentity, + OpaqueBytes.of(0x01), notaryNode.info.notaryIdentity, false)) // Wait for the flow to stop and print @@ -55,8 +56,9 @@ class FxTransactionBuildTutorialTest { // Using NodeB as Issuer create some pounds. val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000), - OpaqueBytes.of(0x01), nodeB.info.legalIdentity, + nodeB.info.legalIdentity, + OpaqueBytes.of(0x01), notaryNode.info.notaryIdentity, false)) // Wait for flow to come to an end and print diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index ef6596cf20..dbc884af41 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -8,6 +8,8 @@ Unreleased * Merged handling of well known and confidential identities in the identity service. +* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. + Milestone 14 ------------ diff --git a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt index 446cca5b4d..7dec787ee6 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt @@ -22,7 +22,7 @@ sealed class CashFlowCommand { val recipient: Party, val notary: Party, val anonymous: Boolean) : CashFlowCommand() { - override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary, anonymous) + override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, recipient, proxy.nodeIdentity().legalIdentity, issueRef, notary, anonymous) } /** diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index 29d51996f5..0fde7d8056 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -18,39 +18,43 @@ import java.util.* * Initiates a flow that produces cash issuance transaction. * * @param amount the amount of currency to issue. - * @param issueRef a reference to put on the issued currency. - * @param recipient the party who should own the currency after it is issued. + * @param issuerBankPartyRef a reference to put on the issued currency. + * @param issueTo the party who should own the currency after it is issued. * @param notary the notary to set on the output states. */ @StartableByRPC class CashIssueFlow(val amount: Amount, - val issueRef: OpaqueBytes, - val recipient: Party, + val issueTo: Party, + val issuerBankParty + : Party, + val issuerBankPartyRef: OpaqueBytes, val notary: Party, val anonymous: Boolean, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, - issueRef: OpaqueBytes, - recipient: Party, - notary: Party) : this(amount, issueRef, recipient, notary, true, tracker()) + issuerBankPartyRef: OpaqueBytes, + issuerBankParty: Party, + issueTo: Party, + notary: Party) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, true, tracker()) constructor(amount: Amount, - issueRef: OpaqueBytes, - recipient: Party, + issueTo: Party, + issuerBankParty: Party, + issuerBankPartyRef: OpaqueBytes, notary: Party, - anonymous: Boolean) : this(amount, issueRef, recipient, notary, anonymous, tracker()) + anonymous: Boolean) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, anonymous, tracker()) @Suspendable override fun call(): AbstractCashFlow.Result { progressTracker.currentStep = GENERATING_ID val txIdentities = if (anonymous) { - subFlow(TransactionKeyFlow(recipient)) + subFlow(TransactionKeyFlow(issueTo)) } else { emptyMap() } - val anonymousRecipient = txIdentities[recipient] ?: recipient + val anonymousRecipient = txIdentities[issueTo] ?: issueTo progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionBuilder(notary) - val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) + val issuer = issuerBankParty.ref(issuerBankPartyRef) val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary) progressTracker.currentStep = SIGNING_TX val tx = serviceHub.signInitialTransaction(builder, signers) diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt deleted file mode 100644 index 51c885d176..0000000000 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ /dev/null @@ -1,123 +0,0 @@ -package net.corda.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.Amount -import net.corda.core.contracts.FungibleAsset -import net.corda.core.contracts.Issued -import net.corda.core.flows.* -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.unwrap -import net.corda.finance.CHF -import net.corda.finance.EUR -import net.corda.finance.GBP -import net.corda.finance.USD -import java.util.* - -/** - * This flow enables a client to request issuance of some [FungibleAsset] from a - * server acting as an issuer (see [Issued]) of FungibleAssets. - * - * It is not intended for production usage, but rather for experimentation and testing purposes where it may be - * useful for creation of fake assets. - */ -object IssuerFlow { - @CordaSerializable - data class IssuanceRequestState(val amount: Amount, - val issueToParty: Party, - val issuerPartyRef: OpaqueBytes, - val notaryParty: Party, - val anonymous: Boolean) - - /** - * IssuanceRequester should be used by a client to ask a remote node to issue some [FungibleAsset] with the given details. - * Returns the transaction created by the Issuer to move the cash to the Requester. - * - * @param anonymous true if the issued asset should be sent to a new confidential identity, false to send it to the - * well known identity (generally this is only used in testing). - */ - @InitiatingFlow - @StartableByRPC - class IssuanceRequester(val amount: Amount, - val issueToParty: Party, - val issueToPartyRef: OpaqueBytes, - val issuerBankParty: Party, - val notaryParty: Party, - val anonymous: Boolean) : FlowLogic() { - @Suspendable - @Throws(CashException::class) - override fun call(): AbstractCashFlow.Result { - val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, notaryParty, anonymous) - return sendAndReceive(issuerBankParty, issueRequest).unwrap { res -> - val tx = res.stx.tx - val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token)) - val cashOutputs = tx.filterOutputs { state -> state.owner == res.recipient } - require(cashOutputs.size == 1) { "Require a single cash output paying ${res.recipient}, found ${tx.outputs}" } - require(cashOutputs.single().amount == expectedAmount) { "Require payment of $expectedAmount"} - res - } - } - } - - /** - * Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client. - * Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester. - */ - @InitiatedBy(IssuanceRequester::class) - class Issuer(val otherParty: Party) : FlowLogic() { - companion object { - object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request") - object ISSUING : ProgressTracker.Step("Self issuing asset") - object TRANSFERRING : ProgressTracker.Step("Transferring asset to issuance requester") - object SENDING_CONFIRM : ProgressTracker.Step("Confirming asset issuance to requester") - - fun tracker() = ProgressTracker(AWAITING_REQUEST, ISSUING, TRANSFERRING, SENDING_CONFIRM) - private val VALID_CURRENCIES = listOf(USD, GBP, EUR, CHF) - } - - override val progressTracker: ProgressTracker = tracker() - - @Suspendable - @Throws(CashException::class) - override fun call(): SignedTransaction { - progressTracker.currentStep = AWAITING_REQUEST - val issueRequest = receive(otherParty).unwrap { - // validate request inputs (for example, lets restrict the types of currency that can be issued) - if (it.amount.token !in VALID_CURRENCIES) throw FlowException("Currency must be one of $VALID_CURRENCIES") - it - } - // TODO: parse request to determine Asset to issue - val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.notaryParty, issueRequest.anonymous) - progressTracker.currentStep = SENDING_CONFIRM - send(otherParty, txn) - return txn.stx - } - - @Suspendable - private fun issueCashTo(amount: Amount, - issueTo: Party, - issuerPartyRef: OpaqueBytes, - notaryParty: Party, - anonymous: Boolean): AbstractCashFlow.Result { - // invoke Cash subflow to issue Asset - progressTracker.currentStep = ISSUING - val issueRecipient = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous) - val issueTx = subFlow(issueCashFlow) - // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) - // short-circuit when issuing to self - if (issueTo == serviceHub.myInfo.legalIdentity) - return issueTx - // now invoke Cash subflow to Move issued assetType to issue requester - progressTracker.currentStep = TRANSFERRING - val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous) - val moveTx = subFlow(moveCashFlow) - // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) - return moveTx - } - } -} \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt index 44688436d6..4a15e5a16d 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -33,9 +33,11 @@ class CashExitFlowTests { bankOfCorda = bankOfCordaNode.info.legalIdentity mockNet.runNetwork() - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, bankOfCorda, - notary)).resultFuture + bankOfCorda, ref, + notary, + anonymous = true)).resultFuture mockNet.runNetwork() future.getOrThrow() } diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt index 9e56aa7105..8bc11139eb 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -42,9 +42,12 @@ class CashIssueFlowTests { fun `issue some cash`() { val expected = 500.DOLLARS val ref = OpaqueBytes.of(0x01) - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, bankOfCorda, - notary)).resultFuture + bankOfCorda, + ref, + notary, + anonymous = true)).resultFuture mockNet.runNetwork() val issueTx = future.getOrThrow().stx val output = issueTx.tx.outputsOfType().single() @@ -54,9 +57,13 @@ class CashIssueFlowTests { @Test fun `issue zero cash`() { val expected = 0.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, OpaqueBytes.of(0x01), + val ref = OpaqueBytes.of(0x01) + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, bankOfCorda, - notary)).resultFuture + bankOfCorda, + ref, + notary, + anonymous = true)).resultFuture mockNet.runNetwork() assertFailsWith { future.getOrThrow() diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index a934d92127..673b6940ac 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -37,9 +37,12 @@ class CashPaymentFlowTests { notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, bankOfCorda, - notary)).resultFuture + bankOfCorda, + ref, + notary, + true)).resultFuture mockNet.runNetwork() future.getOrThrow() } diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt deleted file mode 100644 index 5a76d09aa7..0000000000 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ /dev/null @@ -1,167 +0,0 @@ -package net.corda.flows - -import net.corda.contracts.asset.Cash -import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.Amount -import net.corda.core.flows.FlowException -import net.corda.core.identity.Party -import net.corda.core.node.services.Vault -import net.corda.core.node.services.trackBy -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.getOrThrow -import net.corda.finance.DOLLARS -import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.testing.contracts.calculateRandomlySizedAmounts -import net.corda.testing.expect -import net.corda.testing.expectEvents -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.sequence -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import java.util.* -import kotlin.test.assertFailsWith - -@RunWith(Parameterized::class) -class IssuerFlowTest(val anonymous: Boolean) { - companion object { - @Parameterized.Parameters - @JvmStatic - fun data(): Collection> { - return listOf(arrayOf(false), arrayOf(true)) - } - } - - lateinit var mockNet: MockNetwork - lateinit var notaryNode: MockNode - lateinit var bankOfCordaNode: MockNode - lateinit var bankClientNode: MockNode - - @Before - fun start() { - mockNet = MockNetwork(threadPerNode = true) - val basketOfNodes = mockNet.createSomeNodes(2) - bankOfCordaNode = basketOfNodes.partyNodes[0] - bankClientNode = basketOfNodes.partyNodes[1] - notaryNode = basketOfNodes.notaryNode - } - - @After - fun cleanUp() { - mockNet.stopNodes() - } - - @Test - fun `test issuer flow`() { - val notary = notaryNode.services.myInfo.notaryIdentity - val (vaultUpdatesBoc, vaultUpdatesBankClient) = bankOfCordaNode.database.transaction { - // Register for vault updates - val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) - val (_, vaultUpdatesBankClient) = bankClientNode.services.vaultQueryService.trackBy(criteria) - - // using default IssueTo Party Reference - val issuerResult = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, - bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) - issuerResult.get() - - Pair(vaultUpdatesBoc, vaultUpdatesBankClient) - } - - // Check Bank of Corda Vault Updates - vaultUpdatesBoc.expectEvents { - sequence( - // ISSUE - expect { update -> - require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } - require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } - val issued = update.produced.single().state.data - require(issued.owner.owningKey in bankOfCordaNode.services.keyManagementService.keys) - }, - // MOVE - expect { update -> - require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } - require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" } - } - ) - } - - // Check Bank Client Vault Updates - vaultUpdatesBankClient.expectEvents { - // MOVE - expect { (consumed, produced) -> - require(consumed.isEmpty()) { consumed.size } - require(produced.size == 1) { produced.size } - val paidState = produced.single().state.data - require(paidState.owner.owningKey in bankClientNode.services.keyManagementService.keys) - } - } - } - - @Test - fun `test issuer flow rejects restricted`() { - val notary = notaryNode.services.myInfo.notaryIdentity - // try to issue an amount of a restricted currency - assertFailsWith { - runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, Currency.getInstance("BRL")), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() - } - } - - @Test - fun `test issue flow to self`() { - val notary = notaryNode.services.myInfo.notaryIdentity - val vaultUpdatesBoc = bankOfCordaNode.database.transaction { - val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) - - // using default IssueTo Party Reference - runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, - bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() - vaultUpdatesBoc - } - - // Check Bank of Corda Vault Updates - vaultUpdatesBoc.expectEvents { - sequence( - // ISSUE - expect { update -> - require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } - require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } - } - ) - } - } - - @Test - fun `test concurrent issuer flow`() { - val notary = notaryNode.services.myInfo.notaryIdentity - // this test exercises the Cashflow issue and move subflows to ensure consistent spending of issued states - val amount = 10000.DOLLARS - val amounts = calculateRandomlySizedAmounts(10000.DOLLARS, 10, 10, Random()) - val handles = amounts.map { pennies -> - runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) - } - handles.forEach { - require(it.get().stx is SignedTransaction) - } - } - - private fun runIssuerAndIssueRequester(issuerNode: MockNode, - issueToNode: MockNode, - amount: Amount, - issueToParty: Party, - ref: OpaqueBytes, - notaryParty: Party): CordaFuture { - val issueToPartyAndRef = issueToParty.ref(ref) - val issueRequest = IssuanceRequester(amount, issueToParty, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, notaryParty, - anonymous) - return issueToNode.services.startFlow(issueRequest).resultFuture - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2206df75ba..a5a0b9b7e0 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -29,7 +29,6 @@ import net.corda.core.utilities.toNonEmptySet import net.corda.flows.CashExitFlow import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow -import net.corda.flows.IssuerFlow import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotifyTransactionHandler @@ -210,9 +209,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, findRPCFlows(scanResult) } - // TODO Remove this once the cash stuff is in its own CorDapp - registerInitiatedFlow(IssuerFlow.Issuer::class.java) - runOnStop += network::stop _networkMapRegistrationFuture.captureLater(registerWithNetworkMapIfConfigured()) smm.start() diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 316c661234..431365f7e2 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -95,7 +95,7 @@ class CordaRPCOpsImplTest { // Tell the monitoring service node to issue some cash val anonymous = false val recipient = aliceNode.info.legalIdentity - val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity, anonymous) + val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), recipient, rpc.nodeIdentity().legalIdentity, ref, notaryNode.info.notaryIdentity, anonymous) mockNet.runNetwork() var issueSmId: StateMachineRunId? = null @@ -133,8 +133,9 @@ class CordaRPCOpsImplTest { val anonymous = false val result = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, - OpaqueBytes(ByteArray(1, { 1 })), aliceNode.info.legalIdentity, + rpc.nodeIdentity().legalIdentity, + OpaqueBytes(ByteArray(1, { 1 })), notaryNode.info.notaryIdentity, false ) @@ -213,8 +214,9 @@ class CordaRPCOpsImplTest { assertThatExceptionOfType(PermissionException::class.java).isThrownBy { rpc.startFlow(::CashIssueFlow, Amount(100, USD), - OpaqueBytes(ByteArray(1, { 1 })), aliceNode.info.legalIdentity, + rpc.nodeIdentity().legalIdentity, + OpaqueBytes(ByteArray(1, { 1 })), notaryNode.info.notaryIdentity, false ) diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 8eed891813..d905a603f7 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -330,8 +330,9 @@ class FlowFrameworkTests { assertEquals(notary1.info.notaryIdentity, notary2.info.notaryIdentity) node1.services.startFlow(CashIssueFlow( 2000.DOLLARS, - OpaqueBytes.of(0x01), node1.info.legalIdentity, + node1.info.legalIdentity, + OpaqueBytes.of(0x01), notary1.info.notaryIdentity, anonymous = false)) // We pay a couple of times, the notary picking should go round robin diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 0b1ddf7076..d2a42095f2 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -68,7 +68,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { 'password' : "test", 'permissions': ["StartFlow.net.corda.flows.CashPaymentFlow", "StartFlow.net.corda.flows.CashExitFlow", - "StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"]] + "StartFlow.net.corda.flows.CashIssueFlow"]] ] } node { diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index bfeeb8691f..e804a51777 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -1,14 +1,14 @@ package net.corda.bank import net.corda.contracts.asset.Cash -import net.corda.finance.DOLLARS +import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.getOrThrow -import net.corda.flows.IssuerFlow.IssuanceRequester +import net.corda.finance.DOLLARS +import net.corda.flows.CashIssueFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User @@ -20,7 +20,7 @@ class BankOfCordaRPCClientTest { @Test fun `issuer flow via RPC`() { driver(dsl = { - val bocManager = User("bocManager", "password1", permissions = setOf(startFlowPermission())) + val bocManager = User("bocManager", "password1", permissions = setOf(startFlowPermission())) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet()) val (nodeBankOfCorda, nodeBigCorporation) = listOf( startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type)), listOf(bocManager)), @@ -45,11 +45,11 @@ class BankOfCordaRPCClientTest { // Kick-off actual Issuer Flow val anonymous = true bocProxy.startFlow( - ::IssuanceRequester, + ::CashIssueFlow, 1000.DOLLARS, nodeBigCorporation.nodeInfo.legalIdentity, - BIG_CORP_PARTY_REF, nodeBankOfCorda.nodeInfo.legalIdentity, + BIG_CORP_PARTY_REF, nodeBankOfCorda.nodeInfo.notaryIdentity, anonymous).returnValue.getOrThrow() diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index eda775af3f..4390b85298 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -8,8 +8,8 @@ import net.corda.core.node.services.ServiceType import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.flows.CashExitFlow +import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow -import net.corda.flows.IssuerFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User @@ -68,7 +68,7 @@ private class BankOfCordaDriver { "test", permissions = setOf( startFlowPermission(), - startFlowPermission(), + startFlowPermission(), startFlowPermission())) val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission())) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index c5e6e96263..944585d1bf 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -8,7 +8,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.IssuerFlow.IssuanceRequester +import net.corda.flows.CashIssueFlow import net.corda.testing.http.HttpApi import java.util.* @@ -46,9 +46,9 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { ?: throw IllegalStateException("Unable to locate notary node in network map cache") val amount = Amount(params.amount, Currency.getInstance(params.currency)) - val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) + val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) - return rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryNode.notaryIdentity, params.anonymous) + return rpc.startFlow(::CashIssueFlow, amount, issueToParty, issuerBankParty, issuerBankPartyRef, notaryNode.notaryIdentity, params.anonymous) .returnValue.getOrThrow().stx } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 5524c4ba30..b275d7717d 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -6,7 +6,7 @@ import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor -import net.corda.flows.IssuerFlow.IssuanceRequester +import net.corda.flows.CashIssueFlow import org.bouncycastle.asn1.x500.X500Name import java.time.LocalDateTime import java.util.* @@ -18,7 +18,7 @@ import javax.ws.rs.core.Response @Path("bank") class BankOfCordaWebApi(val rpc: CordaRPCOps) { data class IssueRequestParams(val amount: Long, val currency: String, - val issueToPartyName: X500Name, val issueToPartyRefAsString: String, + val issueToPartyName: X500Name, val issuerBankPartyRef: String, val issuerBankName: X500Name, val notaryName: X500Name, val anonymous: Boolean) @@ -52,13 +52,13 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate $notaryParty in network map service").build() val amount = Amount(params.amount, Currency.getInstance(params.currency)) - val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) + val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) val anonymous = params.anonymous // invoke client side of Issuer Flow: IssuanceRequester // The line below blocks and waits for the future to resolve. return try { - rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() + rpc.startFlow(::CashIssueFlow, amount, issueToParty, issuerBankParty, issuerBankPartyRef, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() logger.info("Issue request completed successfully: $params") Response.status(Response.Status.CREATED).build() } catch (e: Exception) { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 101224dde2..942832f930 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -54,7 +54,8 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { val anonymous = false // issue random amounts of currency up to the requested amount, in parallel val resultFutures = amounts.map { pennies -> - rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), OpaqueBytes.of(1), buyer, notaryNode.notaryIdentity, anonymous).returnValue + rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), buyer, rpc.nodeIdentity().legalIdentity, + OpaqueBytes.of(1), notaryNode.notaryIdentity, anonymous).returnValue } resultFutures.transpose().getOrThrow() diff --git a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index d376d21ca8..ebbe86f80d 100644 --- a/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -2,15 +2,16 @@ package net.corda.traderdemo import net.corda.core.internal.div import net.corda.core.node.services.ServiceInfo -import net.corda.testing.DUMMY_BANK_A -import net.corda.testing.DUMMY_BANK_B -import net.corda.testing.DUMMY_NOTARY -import net.corda.flows.IssuerFlow +import net.corda.flows.CashIssueFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC +import net.corda.testing.DUMMY_BANK_A +import net.corda.testing.DUMMY_BANK_B +import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver +import net.corda.traderdemo.flow.CommercialPaperIssueFlow import net.corda.traderdemo.flow.SellerFlow /** @@ -19,11 +20,13 @@ import net.corda.traderdemo.flow.SellerFlow */ fun main(args: Array) { val permissions = setOf( - startFlowPermission(), + startFlowPermission(), startFlowPermission()) val demoUser = listOf(User("demo", "demo", permissions)) driver(driverDirectory = "build" / "trader-demo-nodes", isDebug = true) { - val user = User("user1", "test", permissions = setOf(startFlowPermission())) + val user = User("user1", "test", permissions = setOf(startFlowPermission(), + startFlowPermission(), + startFlowPermission())) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) startNode(DUMMY_BANK_A.name, rpcUsers = demoUser) startNode(DUMMY_BANK_B.name, rpcUsers = demoUser) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index 0057bbe945..4d16f3ad80 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -26,8 +26,8 @@ import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.model.CashTransaction import net.corda.explorer.model.IssuerModel From b76d0368437b0a7e180b443f6243f5bdd6fd446b Mon Sep 17 00:00:00 2001 From: Ross Nicoll Date: Fri, 11 Aug 2017 17:02:39 +0100 Subject: [PATCH 008/101] Change CashIssueFlow to always issue to ourselves Change CashIssueFlow to always issue to ourselves, and require the cash is then moved in a separate payment operation. This more closely models actual operation inside banks, and is a step towards making all move-like operations go through a uniform verification process. --- .../corda/client/jfx/NodeMonitorModelTest.kt | 9 +---- .../client/rpc/CordaRPCJavaClientTest.java | 2 +- .../corda/client/rpc/CordaRPCClientTest.kt | 19 +++++---- .../rpc/StandaloneCordaRPCJavaClientTest.java | 2 +- .../kotlin/rpc/StandaloneCordaRPClientTest.kt | 16 +++----- .../core/flows/ContractUpgradeFlowTest.kt | 3 +- .../corda/docs/IntegrationTestingTutorial.kt | 39 ++++++++----------- .../net/corda/docs/ClientRpcTutorial.kt | 2 +- .../kotlin/net/corda/docs/CustomVaultQuery.kt | 3 +- .../net/corda/docs/CustomVaultQueryTest.kt | 5 +-- .../docs/FxTransactionBuildTutorialTest.kt | 10 +---- .../kotlin/net/corda/flows/CashFlowCommand.kt | 7 +++- .../kotlin/net/corda/flows/CashIssueFlow.kt | 36 ++++++----------- .../net/corda/flows/CashExitFlowTests.kt | 12 ++---- .../net/corda/flows/CashIssueFlowTests.kt | 14 +------ .../net/corda/flows/CashPaymentFlowTests.kt | 7 +--- .../net/corda/node/NodePerformanceTests.kt | 2 +- .../node/services/DistributedServiceTests.kt | 8 +--- .../net/corda/node/CordaRPCOpsImplTest.kt | 17 ++------ .../statemachine/FlowFrameworkTests.kt | 5 +-- .../corda/bank/BankOfCordaRPCClientTest.kt | 13 +++++-- .../net/corda/bank/BankOfCordaDriver.kt | 4 +- .../corda/bank/api/BankOfCordaClientApi.kt | 9 +++-- .../net/corda/bank/api/BankOfCordaWebApi.kt | 6 ++- .../net/corda/traderdemo/TraderDemoTest.kt | 5 ++- .../corda/traderdemo/TraderDemoClientApi.kt | 7 ++-- .../net/corda/explorer/ExplorerSimulation.kt | 3 +- .../views/cordapps/cash/NewTransaction.kt | 12 +++--- .../net/corda/verifier/VerifierTests.kt | 2 +- 29 files changed, 111 insertions(+), 168 deletions(-) diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 3d98864cb4..dc44744c1e 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -108,14 +108,10 @@ class NodeMonitorModelTest : DriverBasedTest() { @Test fun `cash issue works end to end`() { - val anonymous = false rpc.startFlow(::CashIssueFlow, Amount(100, USD), - aliceNode.legalIdentity, - rpc.nodeIdentity().legalIdentity, OpaqueBytes(ByteArray(1, { 1 })), - notaryNode.notaryIdentity, - anonymous + notaryNode.notaryIdentity ) vaultUpdates.expectEvents(isStrict = false) { @@ -137,8 +133,7 @@ class NodeMonitorModelTest : DriverBasedTest() { @Test fun `cash issue and move`() { val anonymous = false - rpc.startFlow(::CashIssueFlow, 100.DOLLARS, aliceNode.legalIdentity, rpc.nodeIdentity().legalIdentity, OpaqueBytes.of(1), - notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() + rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow() rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow() var issueSmId: StateMachineRunId? = null diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index d849f923b1..b1592f6c29 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -71,7 +71,7 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { FlowHandle flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, DOLLARS(123), OpaqueBytes.of("1".getBytes()), - node.info.getLegalIdentity(), node.info.getLegalIdentity()); + node.info.getLegalIdentity()); System.out.println("Started issuing cash, waiting on result"); flowHandle.getReturnValue().get(); diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 92febedcd0..e3632469a1 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -78,8 +78,7 @@ class CordaRPCClientTest : NodeBasedTest() { println("Creating proxy") println("Starting flow") val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow, - 20.DOLLARS, node.info.legalIdentity, - node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true + 20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity ) println("Started flow, waiting on result") flowHandle.progress.subscribe { @@ -114,8 +113,7 @@ class CordaRPCClientTest : NodeBasedTest() { assertTrue(startCash.isEmpty(), "Should not start with any cash") val flowHandle = proxy.startFlow(::CashIssueFlow, - 123.DOLLARS, node.info.legalIdentity, - node.info.legalIdentity, OpaqueBytes.of(0), node.info.legalIdentity, true + 123.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity ) println("Started issuing cash, waiting on result") flowHandle.returnValue.get() @@ -141,15 +139,16 @@ class CordaRPCClientTest : NodeBasedTest() { } } val nodeIdentity = node.info.legalIdentity - node.services.startFlow(CashIssueFlow(2000.DOLLARS, nodeIdentity, nodeIdentity, OpaqueBytes.of(0), nodeIdentity, true), FlowInitiator.Shell).resultFuture.getOrThrow() + node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow() proxy.startFlow(::CashIssueFlow, - 123.DOLLARS, nodeIdentity, - nodeIdentity, OpaqueBytes.of(0), nodeIdentity, - true + 123.DOLLARS, + OpaqueBytes.of(0), + nodeIdentity ).returnValue.getOrThrow() proxy.startFlowDynamic(CashIssueFlow::class.java, - 1000.DOLLARS, OpaqueBytes.of(0), - nodeIdentity, nodeIdentity).returnValue.getOrThrow() + 1000.DOLLARS, + OpaqueBytes.of(0), + nodeIdentity).returnValue.getOrThrow() assertEquals(2, countRpcFlows) assertEquals(1, countShellFlows) } diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 6c18e31053..38abeae9c7 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -75,7 +75,7 @@ public class StandaloneCordaRPCJavaClientTest { FlowHandle flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, dollars123, OpaqueBytes.of("1".getBytes()), - notaryNode.getLegalIdentity(), notaryNode.getLegalIdentity()); + notaryNode.getLegalIdentity()); System.out.println("Started issuing cash, waiting on result"); flowHandle.getReturnValue().get(); diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 39295148e4..4a6a9dfdf4 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -3,7 +3,6 @@ package net.corda.kotlin.rpc import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import net.corda.client.rpc.CordaRPCConnection -import net.corda.client.rpc.notUsed import net.corda.contracts.asset.Cash import net.corda.contracts.getCashBalance import net.corda.contracts.getCashBalances @@ -96,7 +95,7 @@ class StandaloneCordaRPClientTest { @Test fun `test starting flow`() { - rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) } @@ -104,7 +103,7 @@ class StandaloneCordaRPClientTest { fun `test starting tracked flow`() { var trackCount = 0 val handle = rpcProxy.startTrackedFlow( - ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity + ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.notaryIdentity ) handle.progress.subscribe { msg -> log.info("Flow>> $msg") @@ -133,7 +132,7 @@ class StandaloneCordaRPClientTest { } // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) assertEquals(1, updateCount.get()) } @@ -150,7 +149,7 @@ class StandaloneCordaRPClientTest { } // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) assertNotEquals(0, updateCount.get()) @@ -164,7 +163,7 @@ class StandaloneCordaRPClientTest { @Test fun `test vault query by`() { // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -192,10 +191,7 @@ class StandaloneCordaRPClientTest { val startCash = rpcProxy.getCashBalances() assertTrue(startCash.isEmpty(), "Should not start with any cash") - val flowHandle = rpcProxy.startFlow(::CashIssueFlow, - 629.DOLLARS, OpaqueBytes.of(0), - notaryNode.legalIdentity, notaryNode.legalIdentity - ) + val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 629.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity) println("Started issuing cash, waiting on result") flowHandle.returnValue.get() diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 5925b6c35c..3d9b30e76d 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -175,8 +175,7 @@ class ContractUpgradeFlowTest { @Test fun `upgrade Cash to v2`() { // Create some cash. - val anonymous = false - val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), a.info.legalIdentity, a.info.legalIdentity, OpaqueBytes.of(1), notary, anonymous)).resultFuture + val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture mockNet.runNetwork() val stx = result.getOrThrow().stx val stateAndRef = stx.tx.outRef(0) diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index 6f7f39ceae..a999dcf1ff 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -1,8 +1,6 @@ package net.corda.docs import net.corda.contracts.asset.Cash -import net.corda.core.concurrent.CordaFuture -import net.corda.finance.DOLLARS import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultTrackBy @@ -10,6 +8,7 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.node.services.startFlowPermission @@ -17,13 +16,7 @@ import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.driver -import net.corda.testing.expect -import net.corda.testing.expectEvents -import net.corda.testing.parallel -import net.corda.testing.sequence import org.junit.Test -import java.util.* -import kotlin.concurrent.thread import kotlin.test.assertEquals class IntegrationTestingTutorial { @@ -32,7 +25,8 @@ class IntegrationTestingTutorial { // START 1 driver { val aliceUser = User("aliceUser", "testPassword1", permissions = setOf( - startFlowPermission() + startFlowPermission(), + startFlowPermission() )) val bobUser = User("bobUser", "testPassword2", permissions = setOf( startFlowPermission() @@ -62,20 +56,21 @@ class IntegrationTestingTutorial { // START 4 val issueRef = OpaqueBytes.of(0) - val futures = Stack>() (1..10).map { i -> - thread { - futures.push(aliceProxy.startFlow(::CashIssueFlow, - i.DOLLARS, - bob.nodeInfo.legalIdentity, - alice.nodeInfo.legalIdentity, - issueRef, - notary.nodeInfo.notaryIdentity, - true - ).returnValue) - } - }.forEach(Thread::join) // Ensure the stack of futures is populated. - futures.forEach { it.getOrThrow() } + aliceProxy.startFlow(::CashIssueFlow, + i.DOLLARS, + issueRef, + notary.nodeInfo.notaryIdentity + ).returnValue + }.transpose().getOrThrow() + // We wait for all of the issuances to run before we start making payments + (1..10).map { i -> + aliceProxy.startFlow(::CashPaymentFlow, + i.DOLLARS, + bob.nodeInfo.legalIdentity, + true + ).returnValue + }.transpose().getOrThrow() bobVaultUpdates.expectEvents { parallel( diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index 4a795d38a3..4a1b9d7b6f 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -128,7 +128,7 @@ fun generateTransactions(proxy: CordaRPCOps) { proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me) } else { val quantity = Math.abs(random.nextLong() % 1000) - proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), me, me, issueRef, notary, true) + proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, notary) ownedQuantity += quantity } } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt index 63b6e096e7..1f21300d4c 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt @@ -133,8 +133,7 @@ object TopupIssuerFlow { // invoke Cash subflow to issue Asset progressTracker.currentStep = ISSUING val issuer = serviceHub.myInfo.legalIdentity - val issueRecipient = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issueRecipient, issuer, issuerPartyRef, notaryParty, anonymous = false) + val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, notaryParty) val issueTx = subFlow(issueCashFlow) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // short-circuit when issuing to self diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 87a598eb03..e399763a1a 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -65,11 +65,8 @@ class CustomVaultQueryTest { private fun issueCashForCurrency(amountToIssue: Amount) { // Use NodeA as issuer and create some dollars val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(amountToIssue, - nodeA.info.legalIdentity, - nodeA.info.legalIdentity, OpaqueBytes.of(0x01), - notaryNode.info.notaryIdentity, - anonymous = false)) + notaryNode.info.notaryIdentity)) // Wait for the flow to stop and print flowHandle1.resultFuture.getOrThrow() } diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 7d0b631e10..0e52c47d25 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -45,22 +45,16 @@ class FxTransactionBuildTutorialTest { fun `Run ForeignExchangeFlow to completion`() { // Use NodeA as issuer and create some dollars val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000), - nodeA.info.legalIdentity, - nodeA.info.legalIdentity, OpaqueBytes.of(0x01), - notaryNode.info.notaryIdentity, - false)) + notaryNode.info.notaryIdentity)) // Wait for the flow to stop and print flowHandle1.resultFuture.getOrThrow() printBalances() // Using NodeB as Issuer create some pounds. val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000), - nodeB.info.legalIdentity, - nodeB.info.legalIdentity, OpaqueBytes.of(0x01), - notaryNode.info.notaryIdentity, - false)) + notaryNode.info.notaryIdentity)) // Wait for flow to come to an end and print flowHandle2.resultFuture.getOrThrow() printBalances() diff --git a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt index 7dec787ee6..005a232ec4 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt @@ -6,11 +6,13 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import java.util.* /** * A command to initiate the cash flow with. */ +@Deprecated("Please use the flows directly, these will be removed in a later release") sealed class CashFlowCommand { abstract fun startFlow(proxy: CordaRPCOps): FlowHandle @@ -22,7 +24,10 @@ sealed class CashFlowCommand { val recipient: Party, val notary: Party, val anonymous: Boolean) : CashFlowCommand() { - override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, recipient, proxy.nodeIdentity().legalIdentity, issueRef, notary, anonymous) + override fun startFlow(proxy: CordaRPCOps): FlowHandle { + proxy.startFlow(::CashIssueFlow, amount, issueRef, notary).returnValue.getOrThrow() + return proxy.startFlow(::CashPaymentFlow, amount, recipient, anonymous) + } } /** diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index 0fde7d8056..f8d955e7ee 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -15,51 +15,37 @@ import net.corda.core.utilities.ProgressTracker import java.util.* /** - * Initiates a flow that produces cash issuance transaction. + * Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction). + * + * We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk + * checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash + * and immediately transfer it, so impact of this limitation is considered minimal. * * @param amount the amount of currency to issue. * @param issuerBankPartyRef a reference to put on the issued currency. - * @param issueTo the party who should own the currency after it is issued. * @param notary the notary to set on the output states. */ @StartableByRPC class CashIssueFlow(val amount: Amount, - val issueTo: Party, - val issuerBankParty - : Party, val issuerBankPartyRef: OpaqueBytes, val notary: Party, - val anonymous: Boolean, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, issuerBankPartyRef: OpaqueBytes, - issuerBankParty: Party, - issueTo: Party, - notary: Party) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, true, tracker()) - constructor(amount: Amount, - issueTo: Party, - issuerBankParty: Party, - issuerBankPartyRef: OpaqueBytes, - notary: Party, - anonymous: Boolean) : this(amount, issueTo, issuerBankParty, issuerBankPartyRef, notary, anonymous, tracker()) + notary: Party) : this(amount, issuerBankPartyRef, notary, tracker()) @Suspendable override fun call(): AbstractCashFlow.Result { - progressTracker.currentStep = GENERATING_ID - val txIdentities = if (anonymous) { - subFlow(TransactionKeyFlow(issueTo)) - } else { - emptyMap() - } - val anonymousRecipient = txIdentities[issueTo] ?: issueTo + val issuerCert = serviceHub.myInfo.legalIdentityAndCert + progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionBuilder(notary) - val issuer = issuerBankParty.ref(issuerBankPartyRef) - val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary) + val issuer = issuerCert.party.ref(issuerBankPartyRef) + val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), issuerCert.party, notary) progressTracker.currentStep = SIGNING_TX val tx = serviceHub.signInitialTransaction(builder, signers) progressTracker.currentStep = FINALISING_TX subFlow(FinalityFlow(tx)) - return Result(tx, anonymousRecipient) + return Result(tx, issuerCert.party) } } diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt index 4a15e5a16d..e73838e14a 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -33,11 +33,7 @@ class CashExitFlowTests { bankOfCorda = bankOfCordaNode.info.legalIdentity mockNet.runNetwork() - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, - bankOfCorda, - bankOfCorda, ref, - notary, - anonymous = true)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture mockNet.runNetwork() future.getOrThrow() } @@ -50,8 +46,7 @@ class CashExitFlowTests { @Test fun `exit some cash`() { val exitAmount = 500.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, - ref)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture mockNet.runNetwork() val exitTx = future.getOrThrow().stx.tx val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) @@ -64,8 +59,7 @@ class CashExitFlowTests { @Test fun `exit zero cash`() { val expected = 0.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, - ref)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, ref)).resultFuture mockNet.runNetwork() assertFailsWith { future.getOrThrow() diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt index 8bc11139eb..268464ef93 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -42,12 +42,7 @@ class CashIssueFlowTests { fun `issue some cash`() { val expected = 500.DOLLARS val ref = OpaqueBytes.of(0x01) - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, - bankOfCorda, - bankOfCorda, - ref, - notary, - anonymous = true)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture mockNet.runNetwork() val issueTx = future.getOrThrow().stx val output = issueTx.tx.outputsOfType().single() @@ -58,12 +53,7 @@ class CashIssueFlowTests { fun `issue zero cash`() { val expected = 0.DOLLARS val ref = OpaqueBytes.of(0x01) - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, - bankOfCorda, - bankOfCorda, - ref, - notary, - anonymous = true)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture mockNet.runNetwork() assertFailsWith { future.getOrThrow() diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index 673b6940ac..35d184d766 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -37,12 +37,7 @@ class CashPaymentFlowTests { notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, - bankOfCorda, - bankOfCorda, - ref, - notary, - true)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture mockNet.runNetwork() future.getOrThrow() } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index c4b0d7473c..7ef947df42 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -111,7 +111,7 @@ class NodePerformanceTests { a.rpcClientToNode().use("A", "A") { connection -> println("ISSUING") val doneFutures = (1..100).toList().parallelStream().map { - connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), a.nodeInfo.legalIdentity, a.nodeInfo.notaryIdentity).returnValue + connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), a.nodeInfo.notaryIdentity).returnValue }.toList() doneFutures.transpose().get() println("STARTING PAYMENT") diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index 7c94d3b2ca..966c535dd6 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -134,14 +134,10 @@ class DistributedServiceTests : DriverBasedTest() { } private fun issueCash(amount: Amount) { - val issueHandle = aliceProxy.startFlow( - ::CashIssueFlow, - amount, OpaqueBytes.of(0), alice.nodeInfo.legalIdentity, raftNotaryIdentity) - issueHandle.returnValue.getOrThrow() + aliceProxy.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(0), raftNotaryIdentity).returnValue.getOrThrow() } private fun paySelf(amount: Amount) { - val payHandle = aliceProxy.startFlow(::CashPaymentFlow, amount, alice.nodeInfo.legalIdentity) - payHandle.returnValue.getOrThrow() + aliceProxy.startFlow(::CashPaymentFlow, amount, alice.nodeInfo.legalIdentity).returnValue.getOrThrow() } } diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 431365f7e2..3ac57f57b4 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -93,9 +93,8 @@ class CordaRPCOpsImplTest { } // Tell the monitoring service node to issue some cash - val anonymous = false val recipient = aliceNode.info.legalIdentity - val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), recipient, rpc.nodeIdentity().legalIdentity, ref, notaryNode.info.notaryIdentity, anonymous) + val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, notaryNode.info.notaryIdentity) mockNet.runNetwork() var issueSmId: StateMachineRunId? = null @@ -133,11 +132,8 @@ class CordaRPCOpsImplTest { val anonymous = false val result = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, - aliceNode.info.legalIdentity, - rpc.nodeIdentity().legalIdentity, OpaqueBytes(ByteArray(1, { 1 })), - notaryNode.info.notaryIdentity, - false + notaryNode.info.notaryIdentity ) mockNet.runNetwork() @@ -212,14 +208,7 @@ class CordaRPCOpsImplTest { fun `cash command by user not permissioned for cash`() { CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = emptySet()))) assertThatExceptionOfType(PermissionException::class.java).isThrownBy { - rpc.startFlow(::CashIssueFlow, - Amount(100, USD), - aliceNode.info.legalIdentity, - rpc.nodeIdentity().legalIdentity, - OpaqueBytes(ByteArray(1, { 1 })), - notaryNode.info.notaryIdentity, - false - ) + rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), notaryNode.info.notaryIdentity) } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index d905a603f7..2ebb201c7d 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -330,11 +330,8 @@ class FlowFrameworkTests { assertEquals(notary1.info.notaryIdentity, notary2.info.notaryIdentity) node1.services.startFlow(CashIssueFlow( 2000.DOLLARS, - node1.info.legalIdentity, - node1.info.legalIdentity, OpaqueBytes.of(0x01), - notary1.info.notaryIdentity, - anonymous = false)) + notary1.info.notaryIdentity)) // We pay a couple of times, the notary picking should go round robin for (i in 1..3) { val flow = node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity, anonymous = false)) diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index e804a51777..f5c9da331e 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -9,6 +9,7 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User @@ -20,7 +21,9 @@ class BankOfCordaRPCClientTest { @Test fun `issuer flow via RPC`() { driver(dsl = { - val bocManager = User("bocManager", "password1", permissions = setOf(startFlowPermission())) + val bocManager = User("bocManager", "password1", permissions = setOf( + startFlowPermission(), + startFlowPermission())) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet()) val (nodeBankOfCorda, nodeBigCorporation) = listOf( startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type)), listOf(bocManager)), @@ -47,10 +50,12 @@ class BankOfCordaRPCClientTest { bocProxy.startFlow( ::CashIssueFlow, 1000.DOLLARS, - nodeBigCorporation.nodeInfo.legalIdentity, - nodeBankOfCorda.nodeInfo.legalIdentity, BIG_CORP_PARTY_REF, - nodeBankOfCorda.nodeInfo.notaryIdentity, + nodeBankOfCorda.nodeInfo.notaryIdentity).returnValue.getOrThrow() + bocProxy.startFlow( + ::CashPaymentFlow, + 1000.DOLLARS, + nodeBigCorporation.nodeInfo.legalIdentity, anonymous).returnValue.getOrThrow() // Check Bank of Corda Vault Updates diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index 4390b85298..894e3dc954 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -70,7 +70,9 @@ private class BankOfCordaDriver { startFlowPermission(), startFlowPermission(), startFlowPermission())) - val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission())) + val bigCorpUser = User(BIGCORP_USERNAME, "test", + permissions = setOf( + startFlowPermission())) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) val bankOfCorda = startNode( BOC.name, diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index 944585d1bf..77e9d61cbd 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -9,6 +9,7 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow import net.corda.testing.http.HttpApi import java.util.* @@ -28,6 +29,8 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { /** * RPC API + * + * @return a pair of the issuing and payment transactions. */ fun requestRPCIssue(params: IssueRequestParams): SignedTransaction { val client = CordaRPCClient(hostAndPort) @@ -38,8 +41,6 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { // Resolve parties via RPC val issueToParty = rpc.partyFromX500Name(params.issueToPartyName) ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") - val issuerBankParty = rpc.partyFromX500Name(params.issuerBankName) - ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") val notaryLegalIdentity = rpc.partyFromX500Name(params.notaryName) ?: throw IllegalStateException("Unable to locate ${params.notaryName} in Network Map Service") val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity) @@ -48,7 +49,9 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { val amount = Amount(params.amount, Currency.getInstance(params.currency)) val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) - return rpc.startFlow(::CashIssueFlow, amount, issueToParty, issuerBankParty, issuerBankPartyRef, notaryNode.notaryIdentity, params.anonymous) + rpc.startFlow(::CashIssueFlow, amount, issuerBankPartyRef, notaryNode.notaryIdentity) + .returnValue.getOrThrow().stx + return rpc.startFlow(::CashPaymentFlow, amount, issueToParty, params.anonymous) .returnValue.getOrThrow().stx } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index b275d7717d..8ce2a36de9 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -7,6 +7,7 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow import org.bouncycastle.asn1.x500.X500Name import java.time.LocalDateTime import java.util.* @@ -53,12 +54,13 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { val amount = Amount(params.amount, Currency.getInstance(params.currency)) val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) - val anonymous = params.anonymous // invoke client side of Issuer Flow: IssuanceRequester // The line below blocks and waits for the future to resolve. return try { - rpc.startFlow(::CashIssueFlow, amount, issueToParty, issuerBankParty, issuerBankPartyRef, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() + rpc.startFlow(::CashIssueFlow, amount, issuerBankPartyRef, notaryNode.notaryIdentity).returnValue.getOrThrow() + rpc.startFlow(::CashPaymentFlow, amount, issueToParty, params.anonymous) + .returnValue.getOrThrow().stx logger.info("Issue request completed successfully: $params") Response.status(Response.Status.CREATED).build() } catch (e: Exception) { diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index af2a0b034c..03f5f65ac1 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -7,6 +7,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.millis import net.corda.finance.DOLLARS import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User @@ -27,7 +28,9 @@ class TraderDemoTest : NodeBasedTest() { @Test fun `runs trader demo`() { val demoUser = User("demo", "demo", setOf(startFlowPermission())) - val bankUser = User("user1", "test", permissions = setOf(startFlowPermission(), + val bankUser = User("user1", "test", permissions = setOf( + startFlowPermission(), + startFlowPermission(), startFlowPermission())) val (nodeA, nodeB, bankNode) = listOf( startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)), diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 942832f930..ccc1c6188a 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -16,6 +16,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.DOLLARS import net.corda.finance.USD import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow import net.corda.node.services.vault.VaultSchemaV1 import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.calculateRandomlySizedAmounts @@ -52,10 +53,10 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { ?: throw IllegalStateException("Unable to locate notary node in network map cache") val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random()) val anonymous = false - // issue random amounts of currency up to the requested amount, in parallel + rpc.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow() + // pay random amounts of currency up to the requested amount, in parallel val resultFutures = amounts.map { pennies -> - rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), buyer, rpc.nodeIdentity().legalIdentity, - OpaqueBytes.of(1), notaryNode.notaryIdentity, anonymous).returnValue + rpc.startFlow(::CashPaymentFlow, amount.copy(quantity = pennies), buyer, anonymous).returnValue } resultFutures.transpose().getOrThrow() diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index 54f19f966a..2d3644d636 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -38,8 +38,7 @@ class ExplorerSimulation(val options: OptionSet) { val manager = User("manager", "test", permissions = setOf( startFlowPermission(), startFlowPermission(), - startFlowPermission(), - startFlowPermission()) + startFlowPermission()) ) lateinit var notaryNode: NodeHandle diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index 4d16f3ad80..bb5a9fa07a 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -36,8 +36,9 @@ import net.corda.explorer.views.bigDecimalFormatter import net.corda.explorer.views.byteFormatter import net.corda.explorer.views.stringConverter import net.corda.flows.AbstractCashFlow +import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow import net.corda.flows.CashFlowCommand -import net.corda.flows.IssuerFlow.IssuanceRequester import org.controlsfx.dialog.ExceptionDialog import tornadofx.* import java.math.BigDecimal @@ -94,12 +95,13 @@ class NewTransaction : Fragment() { show() } val handle: FlowHandle = if (command is CashFlowCommand.IssueCash) { - rpcProxy.value!!.startFlow(::IssuanceRequester, + rpcProxy.value!!.startFlow(::CashIssueFlow, + command.amount, + command.issueRef, + command.notary) + rpcProxy.value!!.startFlow(::CashPaymentFlow, command.amount, command.recipient, - command.issueRef, - myIdentity.value!!.legalIdentity, - command.notary, command.anonymous) } else { command.startFlow(rpcProxy.value!!) diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 69e3f42738..b127d5bda1 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -118,7 +118,7 @@ class VerifierTests { val alice = aliceFuture.get() val notary = notaryFuture.get() startVerifier(notary) - alice.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), alice.nodeInfo.legalIdentity, notaryFuture.get().nodeInfo.notaryIdentity).returnValue.get() + alice.rpc.startFlow(::CashIssueFlow, 10.DOLLARS, OpaqueBytes.of(0), notaryFuture.get().nodeInfo.notaryIdentity).returnValue.get() notary.waitUntilNumberOfVerifiers(1) for (i in 1..10) { alice.rpc.startFlow(::CashPaymentFlow, 10.DOLLARS, alice.nodeInfo.legalIdentity).returnValue.get() From 435dcf6af435bf4ff0af1b9ff17ae3a38f3036f9 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Wed, 16 Aug 2017 08:36:00 +0100 Subject: [PATCH 009/101] Fixes the hello, world tutorial. --- docs/source/hello-world-contract.rst | 22 ++++++++------ docs/source/hello-world-flow.rst | 44 +++++++++++++++------------- docs/source/hello-world-running.rst | 5 ++++ docs/source/hello-world-state.rst | 21 ++++++------- docs/source/hello-world-template.rst | 22 ++++---------- 5 files changed, 56 insertions(+), 58 deletions(-) diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 31bebc2495..253020441e 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -79,18 +79,19 @@ Defining IOUContract -------------------- Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or -``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: +``App.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: .. container:: codeset .. code-block:: kotlin - package com.iou + ... import net.corda.core.contracts.* - import net.corda.core.crypto.SecureHash - open class IOUContract : Contract { + ... + + class IOUContract : Contract { // Our Create command. class Create : CommandData @@ -109,7 +110,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi // Constraints on the signers. "There must only be one signer." using (command.signers.toSet().size == 1) - "The signer must be the borrower." using (command.signers.contains(out.borrower.owningKey)) + "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) } } @@ -119,9 +120,10 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi .. code-block:: java - package com.iou; + package com.template.contract; import com.google.common.collect.ImmutableSet; + import com.template.state.IOUState; import net.corda.core.contracts.AuthenticatedObject; import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; @@ -146,7 +148,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().getData().get(0); + final IOUState out = (IOUState) tx.getOutputs().get(0).getData(); final Party lender = out.getLender(); final Party borrower = out.getBorrower(); check.using("The IOU's value must be non-negative.",out.getValue() > 0); @@ -154,7 +156,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi // Constraints on the signers. check.using("There must only be one signer.", ImmutableSet.of(command.getSigners()).size() == 1); - check.using("The signer must be the borrower.", command.getSigners().contains(borrower.getOwningKey())); + check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey())); return null; }); @@ -165,6 +167,8 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi @Override public final SecureHash getLegalContractReference() { return legalContractReference; } } +If you're following along in Java, you'll also need to rename ``TemplateContract.java`` to ``IOUContract.java``. + Let's walk through this code step by step. The Create command @@ -254,7 +258,7 @@ other statements - in this case, we're extracting the transaction's single ``IOU Signer constraints ~~~~~~~~~~~~~~~~~~ -Finally, we require the borrower's signature on the transaction. A transaction's required signers is equal to the union +Finally, we require the lender's signature on the transaction. A transaction's required signers is equal to the union of all the signers listed on the commands. We therefore extract the signers from the ``Create`` command we retrieved earlier. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index c86c81191e..1adb34a1e5 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -33,24 +33,20 @@ FlowLogic Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding ``FlowLogic.call``. -We'll write our flow in either ``TemplateFlow.java`` or ``TemplateFlow.kt``. Overwrite the existing template code with -the following: +We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Overwrite both the existing flows in the template +with the following: .. container:: codeset .. code-block:: kotlin - package com.iou + ... - import co.paralleluniverse.fibers.Suspendable - import net.corda.core.contracts.Command - import net.corda.core.flows.FlowLogic - import net.corda.core.flows.InitiatingFlow - import net.corda.core.flows.StartableByRPC - import net.corda.core.identity.Party - import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker - import net.corda.core.flows.FinalityFlow + import net.corda.core.transactions.TransactionBuilder + import net.corda.core.flows.* + + ... @InitiatingFlow @StartableByRPC @@ -79,7 +75,7 @@ the following: txBuilder.verify(serviceHub) // Signing the transaction. - val signedTx = serviceHub.toSignedTransaction(txBuilder) + val signedTx = serviceHub.signInitialTransaction(txBuilder) // Finalising the transaction. subFlow(FinalityFlow(signedTx)) @@ -88,19 +84,17 @@ the following: .. code-block:: java - package com.iou; + package com.template.flow; import co.paralleluniverse.fibers.Suspendable; + import com.template.contract.IOUContract; + import com.template.state.IOUState; import net.corda.core.contracts.Command; - import net.corda.core.flows.FlowException; - import net.corda.core.flows.FlowLogic; - import net.corda.core.flows.InitiatingFlow; - import net.corda.core.flows.StartableByRPC; + import net.corda.core.flows.*; import net.corda.core.identity.Party; import net.corda.core.transactions.SignedTransaction; import net.corda.core.transactions.TransactionBuilder; import net.corda.core.utilities.ProgressTracker; - import net.corda.flows.FinalityFlow; @InitiatingFlow @StartableByRPC @@ -118,6 +112,11 @@ the following: this.otherParty = otherParty; } + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + /** * The flow logic is encapsulated within the call() method. */ @@ -128,7 +127,7 @@ the following: final Party me = getServiceHub().getMyInfo().getLegalIdentity(); final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - // We create a transaction builder + // We create a transaction builder. final TransactionBuilder txBuilder = new TransactionBuilder(); txBuilder.setNotary(notary); @@ -141,7 +140,7 @@ the following: txBuilder.verify(getServiceHub()); // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().toSignedTransaction(txBuilder); + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Finalising the transaction. subFlow(new FinalityFlow(signedTx)); @@ -150,6 +149,8 @@ the following: } } +If you're following along in Java, you'll also need to rename ``TemplateFlow.java`` to ``IOUFlow.java``. + We now have our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. There's a few things to note: * ``FlowLogic.call`` has a return type that matches the type parameter passed to ``FlowLogic`` - this is type returned @@ -163,6 +164,9 @@ We now have our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. Th * ``@InitiatingFlow`` means that this flow can be started directly by the node * ``StartableByRPC`` allows the node owner to start this flow via an RPC call +* We override the progress tracker, even though we are not providing any progress tracker steps yet. The progress + tracker is required for the node shell to establish when the flow has ended + Let's walk through the steps of ``FlowLogic.call`` one-by-one: Retrieving participant information diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index b71eefd867..2696af6b23 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -9,6 +9,11 @@ Running our CorDapp Now that we've written a CorDapp, it's time to test it by running it on some real Corda nodes. +Clean up +-------- +Before running our node, delete the ``client/TemplateClient.java`` (for Java) or ``client/TemplateClient.kt`` (for +Kotlin) file. We won't be using it, and it will cause build errors unless we remove it. + Deploying our CorDapp --------------------- Let's take a look at the nodes we're going to deploy. Open the project's build file under ``java-source/build.gradle`` diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 3888d07ee9..a4ba0ef205 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -64,31 +64,26 @@ you wish to add them later, its as simple as adding an additional property to yo Defining IOUState ----------------- -Let's open ``TemplateState.java`` (for Java) or ``TemplateState.kt`` (for Kotlin) and update ``TemplateState`` to +Let's open ``TemplateState.java`` (for Java) or ``App.kt`` (for Kotlin) and update ``TemplateState`` to define an ``IOUState``: .. container:: codeset .. code-block:: kotlin - package com.iou - - import net.corda.core.contracts.ContractState - import net.corda.core.identity.Party - class IOUState(val value: Int, val lender: Party, - val borrower: Party, - override val contract: TemplateContract) : ContractState { - + val borrower: Party) : ContractState { + override val contract = TemplateContract() override val participants get() = listOf(lender, borrower) } .. code-block:: java - package com.iou; + package com.template.state; import com.google.common.collect.ImmutableList; + import com.template.contract.TemplateContract; import net.corda.core.contracts.ContractState; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.Party; @@ -99,7 +94,7 @@ define an ``IOUState``: private final int value; private final Party lender; private final Party borrower; - private final IOUContract contract = new IOUContract(); + private final TemplateContract contract = new TemplateContract(); public IOUState(int value, Party lender, Party borrower) { this.value = value; @@ -121,7 +116,7 @@ define an ``IOUState``: @Override // TODO: Once we've defined IOUContract, come back and update this. - public IOUContract getContract() { + public TemplateContract getContract() { return contract; } @@ -131,6 +126,8 @@ define an ``IOUState``: } } +If you're following along in Java, you'll also need to rename ``TemplateState.java`` to ``IOUState.java``. + We've made the following changes: * We've renamed ``TemplateState`` to ``IOUState`` diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 05b9a31224..6ef1be4905 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -25,11 +25,11 @@ Open a terminal window in the directory where you want to download the CorDapp t .. code-block:: text # Clone the template from GitHub: - git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template + git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java *or* - git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template + git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin # Retrieve a list of the stable Milestone branches using: git branch -a --list *release-M* @@ -39,11 +39,8 @@ Open a terminal window in the directory where you want to download the CorDapp t Template structure ------------------ -We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. If -you want to write the CorDapp in Java, you'll be modifying the files under ``java-source``. If you prefer to use -Kotlin, you'll be modifying the files under ``kotlin-source``. - -To implement our IOU CorDapp, we'll only need to modify three files: +We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. To +implement our IOU CorDapp in Java, we'll only need to modify three files: .. container:: codeset @@ -58,16 +55,7 @@ To implement our IOU CorDapp, we'll only need to modify three files: // 3. The flow java-source/src/main/java/com/template/flow/TemplateFlow.java - .. code-block:: kotlin - - // 1. The state - kotlin-source/src/main/kotlin/com/template/state/TemplateState.kt - - // 2. The contract - kotlin-source/src/main/kotlin/com/template/contract/TemplateContract.kt - - // 3. The flow - kotlin-source/src/main/kotlin/com/template/flow/TemplateFlow.kt +For Kotlin, we'll simply be modifying the ``App.kt`` file. Progress so far --------------- From 1e0a26e8e50be4244683ce29ac42ad84293a39d3 Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Wed, 16 Aug 2017 10:06:46 +0100 Subject: [PATCH 010/101] Ensure transactions in tests have commands (#1200) * Ensure transactions in tests have commands --- .../contracts/LedgerTransactionQueryTests.kt | 7 +- .../contracts/TransactionGraphSearchTests.kt | 7 +- .../net/corda/contracts/asset/Obligation.kt | 10 ++- .../corda/contracts/asset/ObligationTests.kt | 65 ++++++++++++------- .../corda/node/services/NotaryChangeTests.kt | 3 +- .../database/RequeryConfigurationTest.kt | 3 +- .../services/events/ScheduledFlowTests.kt | 8 ++- .../persistence/DBTransactionStorageTests.kt | 4 +- .../statemachine/FlowFrameworkTests.kt | 9 ++- .../transactions/NotaryServiceTests.kt | 31 ++++++--- .../ValidatingNotaryServiceTests.kt | 5 +- .../node/services/vault/VaultWithCashTest.kt | 13 ++-- .../corda/irs/api/NodeInterestRatesTest.kt | 16 +++-- .../kotlin/net/corda/testing/TestConstants.kt | 4 +- .../corda/testing/contracts/VaultFiller.kt | 13 +++- 15 files changed, 135 insertions(+), 63 deletions(-) diff --git a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt index 0c1540d089..71e71fe767 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt @@ -7,6 +7,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockServices import org.junit.Before import org.junit.Test @@ -50,7 +51,11 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() { private fun makeDummyStateAndRef(data: Any): StateAndRef<*> { val dummyState = makeDummyState(data) - val fakeIssueTx = services.signInitialTransaction(TransactionBuilder(notary = DUMMY_NOTARY).addOutputState(dummyState)) + val fakeIssueTx = services.signInitialTransaction( + TransactionBuilder(notary = DUMMY_NOTARY) + .addOutputState(dummyState) + .addCommand(dummyCommand()) + ) services.recordTransactions(fakeIssueTx) val dummyStateRef = StateRef(fakeIssueTx.id, 0) return StateAndRef(TransactionState(dummyState, DUMMY_NOTARY, null), dummyStateRef) diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt index 2e1603b1b8..bfefdaae15 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt @@ -34,14 +34,15 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() { val notaryServices = MockServices(DUMMY_NOTARY_KEY) val originBuilder = TransactionBuilder(DUMMY_NOTARY) - originBuilder.addOutputState(DummyState(random31BitValue())) - originBuilder.addCommand(command, MEGA_CORP_PUBKEY) + .addOutputState(DummyState(random31BitValue())) + .addCommand(command, MEGA_CORP_PUBKEY) val originPtx = megaCorpServices.signInitialTransaction(originBuilder) val originTx = notaryServices.addSignature(originPtx) val inputBuilder = TransactionBuilder(DUMMY_NOTARY) - inputBuilder.addInputState(originTx.tx.outRef(0)) + .addInputState(originTx.tx.outRef(0)) + .addCommand(dummyCommand(MEGA_CORP_PUBKEY)) val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder) val inputTx = megaCorpServices.addSignature(inputPtx) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 2a37bda579..94fd65cb3a 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -488,7 +488,8 @@ class Obligation

: Contract { */ fun generateCloseOutNetting(tx: TransactionBuilder, signer: AbstractParty, - vararg states: State

) { + vararg inputs: StateAndRef>) { + val states = inputs.map { it.state.data } val netState = states.firstOrNull()?.bilateralNetState requireThat { @@ -498,6 +499,7 @@ class Obligation

: Contract { "signer is in the state parties" using (signer in netState!!.partyKeys) } + tx.withItems(*inputs) val out = states.reduce(State

::net) if (out.quantity > 0L) tx.addOutputState(out) @@ -567,10 +569,14 @@ class Obligation

: Contract { fun generatePaymentNetting(tx: TransactionBuilder, issued: Issued>, notary: Party, - vararg states: State

) { + vararg inputs: StateAndRef>) { + val states = inputs.map { it.state.data } requireThat { "all states are in the normal lifecycle state " using (states.all { it.lifecycle == Lifecycle.NORMAL }) } + + tx.withItems(*inputs) + val groups = states.groupBy { it.multilateralNetState } val partyLookup = HashMap() val signers = states.map { it.beneficiary }.union(states.map { it.obligor }).toSet() diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index e46c26259f..d8163cef45 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -69,15 +69,16 @@ class ObligationTests { fun trivial() { transaction { input { inState } - this `fails with` "the amounts balance" tweak { output { outState.copy(quantity = 2000.DOLLARS.quantity) } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } tweak { output { outState } - // No command arguments + command(CHARLIE.owningKey) { DummyCommandData } + // Invalid command this `fails with` "required net.corda.contracts.asset.Obligation.Commands.Move command" } tweak { @@ -225,8 +226,8 @@ class ObligationTests { @Test fun `generate close-out net transaction`() { initialiseTestSerialization() - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) + val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) val tx = TransactionBuilder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() @@ -237,8 +238,8 @@ class ObligationTests { @Test fun `generate close-out net transaction with remainder`() { initialiseTestSerialization() - val obligationAliceToBob = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) + val obligationAliceToBob = getStateAndRef((2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB)) + val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) val tx = TransactionBuilder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() @@ -252,10 +253,10 @@ class ObligationTests { @Test fun `generate payment net transaction`() { initialiseTestSerialization() - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) + val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) val tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + Obligation().generatePaymentNetting(this, obligationAliceToBob.state.data.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() assertEquals(0, tx.outputs.size) } @@ -264,17 +265,25 @@ class ObligationTests { @Test fun `generate payment net transaction with remainder`() { initialiseTestSerialization() - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE) - val tx = TransactionBuilder(null).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + val obligationAliceToBobState = obligationAliceToBob.state.data + val obligationBobToAlice = getStateAndRef((2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE)) + val obligationBobToAliceState = obligationBobToAlice.state.data + val tx = TransactionBuilder(DUMMY_NOTARY).apply { + Obligation().generatePaymentNetting(this, obligationAliceToBobState.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() assertEquals(1, tx.outputs.size) - val expected = obligationBobToAlice.copy(quantity = obligationBobToAlice.quantity - obligationAliceToBob.quantity) + val expected = obligationBobToAliceState.copy(quantity = obligationBobToAliceState.quantity - obligationAliceToBobState.quantity) val actual = tx.getOutput(0) assertEquals(expected, actual) } + private inline fun getStateAndRef(state: T): StateAndRef { + val txState = TransactionState(state, DUMMY_NOTARY) + return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0)) + + } + /** Test generating a transaction to mark outputs as having defaulted. */ @Test fun `generate set lifecycle`() { @@ -597,15 +606,20 @@ class ObligationTests { @Test fun zeroSizedValues() { transaction { - input { inState } - input { inState.copy(quantity = 0L) } - this `fails with` "zero sized inputs" - } - transaction { - input { inState } - output { inState } - output { inState.copy(quantity = 0L) } - this `fails with` "zero sized outputs" + command(CHARLIE.owningKey) { Obligation.Commands.Move() } + tweak { + input { inState } + input { inState.copy(quantity = 0L) } + + this `fails with` "zero sized inputs" + } + tweak { + input { inState } + output { inState } + output { inState.copy(quantity = 0L) } + + this `fails with` "zero sized outputs" + } } } @@ -615,6 +629,7 @@ class ObligationTests { transaction { input { inState } output { outState `issued by` MINI_CORP } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } // Can't mix currencies. @@ -622,6 +637,7 @@ class ObligationTests { input { inState } output { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) } output { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } transaction { @@ -634,6 +650,7 @@ class ObligationTests { ) } output { outState.copy(quantity = 115000) } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } // Can't have superfluous input states from different issuers. @@ -704,12 +721,14 @@ class ObligationTests { // Can't merge them together. tweak { output { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L) } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } // Missing MiniCorp deposit tweak { output { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) } output { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index fd5fff4e00..3ffcee67e6 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -15,6 +15,7 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThatExceptionOfType @@ -168,7 +169,7 @@ fun issueState(node: AbstractNode, notaryNode: AbstractNode): StateAndRef<*> { fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: AbstractNode): StateAndRef { val state = TransactionState(DummyContract.MultiOwnerState(0, listOf(nodeA.info.legalIdentity, nodeB.info.legalIdentity)), notaryNode.info.notaryIdentity) - val tx = TransactionBuilder(notary = notaryNode.info.notaryIdentity).withItems(state) + val tx = TransactionBuilder(notary = notaryNode.info.notaryIdentity).withItems(state, dummyCommand()) val signedByA = nodeA.services.signInitialTransaction(tx) val signedByAB = nodeB.services.addSignature(signedByA) val stx = notaryNode.services.addSignature(signedByAB, notaryNode.services.notaryIdentityKey) diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index db05d880db..958f25d540 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -19,6 +19,7 @@ import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseProperties import net.corda.testing.node.makeTestIdentityService @@ -204,7 +205,7 @@ class RequeryConfigurationTest : TestDependencyInjectionBase() { inputs = listOf(StateRef(SecureHash.randomSHA256(), index)), attachments = emptyList(), outputs = emptyList(), - commands = emptyList(), + commands = listOf(dummyCommand()), notary = DUMMY_NOTARY, timeWindow = null ) diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 97ff1b7182..b57af97ed6 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -22,6 +22,7 @@ import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Assert.assertTrue @@ -71,7 +72,8 @@ class ScheduledFlowTests { val notary = serviceHub.networkMapCache.getAnyNotary() val builder = TransactionBuilder(notary) - builder.withItems(scheduledState) + .addOutputState(scheduledState) + .addCommand(dummyCommand(serviceHub.legalIdentityKey)) val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx, setOf(serviceHub.myInfo.legalIdentity))) } @@ -91,7 +93,9 @@ class ScheduledFlowTests { val notary = state.state.notary val newStateOutput = scheduledState.copy(processed = true) val builder = TransactionBuilder(notary) - builder.withItems(state, newStateOutput) + .addInputState(state) + .addOutputState(newStateOutput) + .addCommand(dummyCommand(serviceHub.legalIdentityKey)) val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx, setOf(scheduledState.source, scheduledState.destination))) } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index ba59ceefa1..448f9c305e 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -4,8 +4,8 @@ import net.corda.core.contracts.StateRef import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignatureMetadata -import net.corda.core.node.services.VaultService import net.corda.core.crypto.TransactionSignature +import net.corda.core.node.services.VaultService import net.corda.core.schemas.MappedSchema import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction @@ -220,7 +220,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)), attachments = emptyList(), outputs = emptyList(), - commands = emptyList(), + commands = listOf(dummyCommand()), notary = DUMMY_NOTARY, timeWindow = null ) diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 2ebb201c7d..5bd205078e 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -593,7 +593,8 @@ class FlowFrameworkTests { @Test fun `wait for transaction`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) - ptx.addOutputState(DummyState()) + .addOutputState(DummyState()) + .addCommand(dummyCommand(node1.services.legalIdentityKey)) val stx = node1.services.signInitialTransaction(ptx) val committerFiber = node1.registerFlowFactory(WaitingFlows.Waiter::class) { @@ -607,7 +608,8 @@ class FlowFrameworkTests { @Test fun `committer throws exception before calling the finality flow`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) - ptx.addOutputState(DummyState()) + .addOutputState(DummyState()) + .addCommand(dummyCommand()) val stx = node1.services.signInitialTransaction(ptx) node1.registerFlowFactory(WaitingFlows.Waiter::class) { @@ -623,7 +625,8 @@ class FlowFrameworkTests { @Test fun `verify vault query service is tokenizable by force checkpointing within a flow`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) - ptx.addOutputState(DummyState()) + .addOutputState(DummyState()) + .addCommand(dummyCommand(node1.services.legalIdentityKey)) val stx = node1.services.signInitialTransaction(ptx) node1.registerFlowFactory(VaultQueryFlow::class) { diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index e72e273dc1..4eb85f9f01 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -16,6 +16,7 @@ import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -50,8 +51,10 @@ class NotaryServiceTests { fun `should sign a unique transaction with a valid time-window`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.setTimeWindow(Instant.now(), 30.seconds) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) + .setTimeWindow(Instant.now(), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -64,7 +67,9 @@ class NotaryServiceTests { fun `should sign a unique transaction without a time-window`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } @@ -77,8 +82,10 @@ class NotaryServiceTests { fun `should report error for transaction with an invalid time-window`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) + .setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -92,7 +99,9 @@ class NotaryServiceTests { fun `should sign identical transaction multiple times (signing is idempotent)`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } @@ -110,12 +119,16 @@ class NotaryServiceTests { fun `should report conflict when inputs are reused across transactions`() { val inputState = issueState(clientNode) val stx = run { - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } val stx2 = run { - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.addInputState(issueState(clientNode)) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addInputState(issueState(clientNode)) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 335ec1048b..dadecbc5f7 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -18,6 +18,7 @@ import net.corda.node.services.network.NetworkMapService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -52,7 +53,9 @@ class ValidatingNotaryServiceTests { fun `should report error for invalid transaction dependency`() { val stx = run { val inputState = issueInvalidState(clientNode, notaryNode.info.notaryIdentity) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index db0d369623..e8fa3c4c91 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -222,6 +222,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + addCommand(dummyCommand(notaryServices.legalIdentityKey)) } val dummyIssue = notaryServices.signInitialTransaction(dummyIssueBuilder) @@ -241,7 +242,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() { // Issue a linear state val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY) - dummyIssueBuilder.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + .addCommand(dummyCommand(notaryServices.legalIdentityKey)) val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) val dummyIssue = services.addSignature(dummyIssuePtx) @@ -251,10 +253,10 @@ class VaultWithCashTest : TestDependencyInjectionBase() { assertThat(vaultQuery.queryBy().states).hasSize(1) // Move the same state - val dummyMoveBuilder = TransactionBuilder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) - addInputState(dummyIssue.tx.outRef(0)) - } + val dummyMoveBuilder = TransactionBuilder(notary = DUMMY_NOTARY) + .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + .addInputState(dummyIssue.tx.outRef(0)) + .addCommand(dummyCommand(notaryServices.legalIdentityKey)) val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) @@ -318,6 +320,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { addOutputState(DummyDealContract.State(ref = "999", participants = listOf(freshIdentity))) addInputState(linearStates.first()) addInputState(deals.first()) + addCommand(dummyCommand(notaryServices.legalIdentityKey)) } val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index 9bed90aa82..ef25bf74c2 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -123,7 +123,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `refuse to sign with no relevant commands`() { database.transaction { - val tx = makeTX() + val tx = makeFullTx() val wtx1 = tx.toWireTransaction() fun filterAllOutputs(elem: Any): Boolean { return when (elem) { @@ -145,7 +145,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `sign successfully`() { database.transaction { - val tx = makeTX() + val tx = makePartialTX() val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first() tx.addCommand(fix, oracle.identity.owningKey) // Sign successfully. @@ -159,7 +159,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `do not sign with unknown fix`() { database.transaction { - val tx = makeTX() + val tx = makePartialTX() val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val badFix = Fix(fixOf, BigDecimal("0.6789")) tx.addCommand(badFix, oracle.identity.owningKey) @@ -173,7 +173,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `do not sign too many leaves`() { database.transaction { - val tx = makeTX() + val tx = makePartialTX() val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first() fun filtering(elem: Any): Boolean { return when (elem) { @@ -191,7 +191,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `empty partial transaction to sign`() { - val tx = makeTX() + val tx = makeFullTx() val wtx = tx.toWireTransaction() val ftx = wtx.buildFilteredTransaction(Predicate { false }) assertFailsWith { oracle.sign(ftx) } @@ -207,7 +207,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { n2.database.transaction { n2.installCordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA } - val tx = TransactionBuilder(null) + val tx = makePartialTX() val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val oracle = n2.info.serviceIdentities(NodeInterestRates.Oracle.type).first() val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1")) @@ -238,6 +238,8 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { } } - private fun makeTX() = TransactionBuilder(DUMMY_NOTARY).withItems( + private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems( 1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `with notary` DUMMY_NOTARY) + + private fun makeFullTx() = makePartialTX().withItems(dummyCommand()) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index 29d79fe802..a3617160df 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -74,7 +74,9 @@ val DUMMY_CA: CertificateAndKeyPair by lazy { CertificateAndKeyPair(cert, DUMMY_CA_KEY) } -fun dummyCommand(vararg signers: PublicKey) = Command(object : TypeOnlyCommandData() {}, signers.toList()) +fun dummyCommand(vararg signers: PublicKey = arrayOf(generateKeyPair().public) ) = Command(DummyCommandData, signers.toList()) + +object DummyCommandData : TypeOnlyCommandData() val DUMMY_IDENTITY_1: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_PARTY) val DUMMY_PARTY: Party get() = Party(X500Name("CN=Dummy,O=Dummy,L=Madrid,C=ES"), DUMMY_KEY_1.public) diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index dfb30e6ebb..b6cfb9daeb 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -4,11 +4,13 @@ package net.corda.testing.contracts import net.corda.contracts.Commodity import net.corda.contracts.DealState -import net.corda.contracts.asset.* +import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.CommodityContract +import net.corda.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.contracts.asset.DUMMY_OBLIGATION_ISSUER import net.corda.core.contracts.* import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignatureMetadata -import net.corda.core.utilities.getOrThrow import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party @@ -18,9 +20,11 @@ import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import net.corda.testing.CHARLIE import net.corda.testing.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY_KEY +import net.corda.testing.dummyCommand import java.security.PublicKey import java.time.Duration import java.time.Instant @@ -38,6 +42,7 @@ fun ServiceHub.fillWithSomeTestDeals(dealIds: List, // Issue a deal state val dummyIssue = TransactionBuilder(notary = notary).apply { addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(me))) + addCommand(dummyCommand()) } val stx = signInitialTransaction(dummyIssue) return@map addSignature(stx, notary.owningKey) @@ -76,6 +81,7 @@ fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, linearNumber = linearNumber, linearBoolean = linearBoolean, linearTimestamp = linearTimestamp)) + addCommand(dummyCommand()) } return@map signInitialTransaction(dummyIssue).withAdditionalSignature(issuerKey, signatureMetadata) @@ -194,6 +200,7 @@ fun ServiceHub.consume(states: List>, notary: P states.forEach { val builder = TransactionBuilder(notary = notary).apply { addInputState(it) + addCommand(dummyCommand(notary.owningKey)) } val consumedTx = signInitialTransaction(builder, notary.owningKey) @@ -205,6 +212,7 @@ fun ServiceHub.consumeAndProduce(stateAndRef: StateAndRef, // Create a txn consuming different contract types var builder = TransactionBuilder(notary = notary).apply { addInputState(stateAndRef) + addCommand(dummyCommand(notary.owningKey)) } val consumedTx = signInitialTransaction(builder, notary.owningKey) @@ -214,6 +222,7 @@ fun ServiceHub.consumeAndProduce(stateAndRef: StateAndRef, builder = TransactionBuilder(notary = notary).apply { addOutputState(DummyLinearContract.State(linearId = stateAndRef.state.data.linearId, participants = stateAndRef.state.data.participants)) + addCommand(dummyCommand(notary.owningKey)) } val producedTx = signInitialTransaction(builder, notary.owningKey) From 4d54d1a8dc97b2b6ecbf2a5487e02b27a2b75f56 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 16 Aug 2017 11:39:07 +0100 Subject: [PATCH 011/101] Review Comments Removing unused default method on the factory, it was added for testing although given there are easier ways to build factories for the tests it was never used and doesn't need leaving in --- .../internal/serialization/AMQPSerializationScheme.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index e0a54e8a8a..eee7ed73c6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -46,13 +46,6 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { }.also { registerCustomSerializers(it) } } - fun getSerializerFactory(): SerializerFactory { - return serializerFactoriesForContexts.computeIfAbsent(Pair( - AllWhitelist, SerializationDefaults.javaClass.classLoader)) { - SerializerFactory(AllWhitelist, SerializationDefaults.javaClass.classLoader) - } - } - override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { val serializerFactory = getSerializerFactory(context) return DeserializationInput(serializerFactory).deserialize(byteSequence, clazz) From b9eac635b881801ce0cf7babf4b58e4a1aceb045 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 16 Aug 2017 12:28:54 +0100 Subject: [PATCH 012/101] Vault Query performance fix (#1256) * Do not query database to maintain list of contract state interfaces to concrete concrete state types (use vault observable to construct same) (note this mechanism is tied to transaction boundaries for visibility of updated states) * Build contract types list from vault rawupdates observable(to avoid waiting for transaction commits). Reverted all JUnits to original state. * Bootstrap map from vault database state (node re-start) Skip reflection for already seen types. * Explicitly close session instances after query execution. * Use auto-closeable to scope sessions. --- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../vault/HibernateQueryCriteriaParser.kt | 4 +- .../services/vault/HibernateVaultQueryImpl.kt | 79 +++++++++++++------ .../net/corda/testing/node/MockServices.kt | 2 +- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a5a0b9b7e0..b734859b2c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -772,7 +772,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val networkMapCache by lazy { InMemoryNetworkMapCache(this) } override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties, configuration.database) } override val vaultQueryService by lazy { - HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), { identityService }), vaultService.updatesPublisher) + HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), { identityService }), vaultService) } // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 2ee2218160..114fc8b792 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -21,7 +21,7 @@ import javax.persistence.criteria.* class HibernateQueryCriteriaParser(val contractType: Class, - val contractTypeMappings: Map>, + val contractTypeMappings: Map>, val criteriaBuilder: CriteriaBuilder, val criteriaQuery: CriteriaQuery, val vaultStates: Root) : IQueryCriteriaParser { @@ -97,7 +97,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, private fun deriveContractTypes(contractStateTypes: Set>? = null): List { val combinedContractStateTypes = contractStateTypes?.plus(contractType) ?: setOf(contractType) combinedContractStateTypes.filter { it.name != ContractState::class.java.name }.let { - val interfaces = it.flatMap { contractTypeMappings[it.name] ?: emptyList() } + val interfaces = it.flatMap { contractTypeMappings[it.name] ?: listOf(it.name) } val concrete = it.filter { !it.isInterface }.map { it.name } return interfaces.plus(concrete) } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index 3bccc8597c..1b0156364b 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -1,16 +1,17 @@ package net.corda.node.services.vault import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.VaultQueryService +import net.corda.core.node.services.VaultService import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT @@ -18,18 +19,18 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import net.corda.node.services.database.HibernateConfiguration +import org.hibernate.Session import org.jetbrains.exposed.sql.transactions.TransactionManager -import rx.subjects.PublishSubject import rx.Observable import java.lang.Exception import java.util.* -import javax.persistence.EntityManager import javax.persistence.Tuple class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, - val updatesPublisher: PublishSubject>) : SingletonSerializeAsToken(), VaultQueryService { + val vault: VaultService) : SingletonSerializeAsToken(), VaultQueryService { companion object { val log = loggerFor() } @@ -37,6 +38,29 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() private val criteriaBuilder = sessionFactory.criteriaBuilder + /** + * Maintain a list of contract state interfaces to concrete types stored in the vault + * for usage in generic queries of type queryBy or queryBy> + */ + private val contractTypeMappings = bootstrapContractStateTypes() + + init { + vault.rawUpdates.subscribe { update -> + update.produced.forEach { + val concreteType = it.state.data.javaClass + log.trace { "State update of type: $concreteType" } + val seen = contractTypeMappings.any { it.value.contains(concreteType.name) } + if (!seen) { + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractTypeMappings.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } + } + } + } + } + @Throws(VaultQueryException::class) override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.Page { log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting") @@ -50,15 +74,12 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, totalStates = results.otherResults[0] as Long } - val session = sessionFactory.withOptions(). - connection(TransactionManager.current().connection). - openSession() + val session = getSession() session.use { val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) - val contractTypeMappings = resolveUniqueContractStateTypes(session) // TODO: revisit (use single instance of parser for all queries) val criteriaParser = HibernateQueryCriteriaParser(contractType, contractTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) @@ -116,41 +137,49 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, } } - private val mutex = ThreadBox({ updatesPublisher }) + private val mutex = ThreadBox({ vault.updatesPublisher }) @Throws(VaultQueryException::class) override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): DataFeed, Vault.Update> { return mutex.locked { val snapshotResults = _queryBy(criteria, paging, sorting, contractType) @Suppress("UNCHECKED_CAST") - val updates = updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractType, snapshotResults.stateTypes) } as Observable> + val updates = vault.updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractType, snapshotResults.stateTypes) } as Observable> DataFeed(snapshotResults, updates) } } + private fun getSession(): Session { + return sessionFactory.withOptions(). + connection(TransactionManager.current().connection). + openSession() + } + /** - * Maintain a list of contract state interfaces to concrete types stored in the vault - * for usage in generic queries of type queryBy or queryBy> + * Derive list from existing vault states and then incrementally update using vault observables */ - fun resolveUniqueContractStateTypes(session: EntityManager): Map> { + fun bootstrapContractStateTypes(): MutableMap> { val criteria = criteriaBuilder.createQuery(String::class.java) val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) criteria.select(vaultStates.get("contractStateClassName")).distinct(true) - val query = session.createQuery(criteria) - val results = query.resultList - val distinctTypes = results.map { it } + val session = getSession() + session.use { + val query = session.createQuery(criteria) + val results = query.resultList + val distinctTypes = results.map { it } - val contractInterfaceToConcreteTypes = mutableMapOf>() - distinctTypes.forEach { it -> - @Suppress("UNCHECKED_CAST") - val concreteType = Class.forName(it) as Class - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { - val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableListOf() }) - contractInterface.add(concreteType.name) + val contractInterfaceToConcreteTypes = mutableMapOf>() + distinctTypes.forEach { type -> + @Suppress("UNCHECKED_CAST") + val concreteType = Class.forName(type) as Class + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } } + return contractInterfaceToConcreteTypes } - return contractInterfaceToConcreteTypes } private fun deriveContractInterfaces(clazz: Class): Set> { diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 43db1c6550..0b4ff61add 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -235,7 +235,7 @@ fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(Com vaultService.notifyAll(txs.map { it.tx }) } - override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher) + override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService) override fun jdbcSession(): Connection = database.createSession() } From 576e1c3c200beae81583468db21bb87100c99870 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Wed, 16 Aug 2017 11:59:20 +0100 Subject: [PATCH 013/101] Workaround to parallel cash payment bug in trader demo --- .../kotlin/net/corda/traderdemo/TraderDemoClientApi.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index ccc1c6188a..5eec7c0820 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -5,7 +5,6 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.getCashBalance import net.corda.core.contracts.Amount import net.corda.core.internal.Emoji -import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultQueryBy @@ -54,12 +53,11 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random()) val anonymous = false rpc.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow() - // pay random amounts of currency up to the requested amount, in parallel - val resultFutures = amounts.map { pennies -> - rpc.startFlow(::CashPaymentFlow, amount.copy(quantity = pennies), buyer, anonymous).returnValue + // Pay random amounts of currency up to the requested amount + amounts.forEach { pennies -> + // TODO This can't be done in parallel, perhaps due to soft-locking issues? + rpc.startFlow(::CashPaymentFlow, amount.copy(quantity = pennies), buyer, anonymous).returnValue.getOrThrow() } - - resultFutures.transpose().getOrThrow() println("Cash issued to buyer") // The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an From 3860b22339ab520016056db3c0ed059513d214ae Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 15 Aug 2017 23:16:24 +0100 Subject: [PATCH 014/101] Made all the members of Crypto.kt static so that Java users aren't forced to use Crypto.INSTANCE. --- .../corda/core/crypto/ContentSignerBuilder.kt | 2 +- .../kotlin/net/corda/core/crypto/Crypto.kt | 274 ++++++++++++------ .../core/crypto/composite/CompositeKey.kt | 4 +- .../crypto/composite/CompositeSignature.kt | 2 +- .../crypto/provider/CordaSecurityProvider.kt | 9 +- .../net/corda/core/crypto/CryptoUtilsTest.kt | 2 +- .../net/corda/node/utilities/X509Utilities.kt | 4 +- 7 files changed, 194 insertions(+), 103 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt index cf679e8a7d..8a62f308c4 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt @@ -13,7 +13,7 @@ import java.security.Signature * This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm. */ object ContentSignerBuilder { - fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner { + fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner { val sigAlgId = signatureScheme.signatureOID val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply { if (random != null) { diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 6db86fb563..8b1d9fb542 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -70,6 +70,7 @@ object Crypto { * RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function. * Note: Recommended key size >= 3072 bits. */ + @JvmField val RSA_SHA256 = SignatureScheme( 1, "RSA_SHA256", @@ -84,6 +85,7 @@ object Crypto { ) /** ECDSA signature scheme using the secp256k1 Koblitz curve. */ + @JvmField val ECDSA_SECP256K1_SHA256 = SignatureScheme( 2, "ECDSA_SECP256K1_SHA256", @@ -98,6 +100,7 @@ object Crypto { ) /** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */ + @JvmField val ECDSA_SECP256R1_SHA256 = SignatureScheme( 3, "ECDSA_SECP256R1_SHA256", @@ -112,6 +115,7 @@ object Crypto { ) /** EdDSA signature scheme using the ed255519 twisted Edwards curve. */ + @JvmField val EDDSA_ED25519_SHA512 = SignatureScheme( 4, "EDDSA_ED25519_SHA512", @@ -131,7 +135,10 @@ object Crypto { * SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers * at the cost of larger key sizes and loss of compatibility. */ + @JvmField val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256)) + + @JvmField val SPHINCS256_SHA256 = SignatureScheme( 5, "SPHINCS-256_SHA512", @@ -146,13 +153,12 @@ object Crypto { "at the cost of larger key sizes and loss of compatibility." ) - /** - * Corda composite key type - */ + /** Corda composite key type */ + @JvmField val COMPOSITE_KEY = SignatureScheme( 6, "COMPOSITE", - AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), + AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY), emptyList(), CordaSecurityProvider.PROVIDER_NAME, CompositeKey.KEY_ALGORITHM, @@ -163,13 +169,14 @@ object Crypto { ) /** Our default signature scheme if no algorithm is specified (e.g. for key generation). */ + @JvmField val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 /** * Supported digital signature schemes. * Note: Only the schemes added in this map will be supported (see [Crypto]). */ - val supportedSignatureSchemes = listOf( + private val signatureSchemeMap: Map = listOf( RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, @@ -183,15 +190,15 @@ object Crypto { * algorithm identifiers. */ private val algorithmMap: Map - = (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } } - + supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) }) + = (signatureSchemeMap.values.flatMap { scheme -> scheme.alternativeOIDs.map { Pair(it, scheme) } } + + signatureSchemeMap.values.map { Pair(it.signatureOID, it) }) .toMap() // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // that could cause unexpected and suspicious behaviour. // i.e. if someone removes a Provider and then he/she adds a new one with the same name. // The val is private to avoid any harmful state changes. - val providerMap: Map = mapOf( + private val providerMap: Map = mapOf( BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(), CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(), "BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. @@ -201,6 +208,14 @@ object Crypto { addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512)) } + @JvmStatic + fun supportedSignatureSchemes(): List = ArrayList(signatureSchemeMap.values) + + @JvmStatic + fun findProvider(name: String): Provider { + return providerMap[name] ?: throw IllegalArgumentException("Unrecognised provider: $name") + } + init { // This registration is needed for reading back EdDSA key from java keystore. // TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider. @@ -219,8 +234,10 @@ object Crypto { } } + @JvmStatic fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme { - return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}") + return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] + ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}") } /** @@ -231,8 +248,11 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(IllegalArgumentException::class) - fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") + @JvmStatic + fun findSignatureScheme(schemeCodeName: String): SignatureScheme { + return signatureSchemeMap[schemeCodeName] + ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") + } /** * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key]. @@ -242,7 +262,7 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested key type is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun findSignatureScheme(key: PublicKey): SignatureScheme { val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded) return findSignatureScheme(keyInfo.algorithm) @@ -256,7 +276,7 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested key type is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun findSignatureScheme(key: PrivateKey): SignatureScheme { val keyInfo = PrivateKeyInfo.getInstance(key.encoded) return findSignatureScheme(keyInfo.privateKeyAlgorithm) @@ -269,11 +289,12 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { val keyInfo = PrivateKeyInfo.getInstance(encodedKey) val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm) - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } /** @@ -284,8 +305,11 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) - fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey) + @JvmStatic + @Throws(InvalidKeySpecException::class) + fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey { + return decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey) + } /** * Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name. @@ -295,13 +319,18 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) + @JvmStatic + @Throws(InvalidKeySpecException::class) fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } try { - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse) + throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that " + + "it corresponds to the input scheme's code name.", ikse) } } @@ -312,11 +341,12 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun decodePublicKey(encodedKey: ByteArray): PublicKey { val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey) val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm) - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) } /** @@ -328,8 +358,11 @@ object Crypto { * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) - fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey) + @JvmStatic + @Throws(InvalidKeySpecException::class) + fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey { + return decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey) + } /** * Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name. @@ -340,19 +373,25 @@ object Crypto { * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) + @JvmStatic + @Throws(InvalidKeySpecException::class) fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } try { - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse) + throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and " + + "that it corresponds to the input scheme's code name.", ikse) } } /** * Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based - * on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray). + * on the [PrivateKey] type, but if the schemeCodeName is known, then better use + * doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray). * @param privateKey the signer's [PrivateKey]. * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). * @return the digital signature (in [ByteArray]) on the input message. @@ -360,7 +399,8 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData) /** @@ -373,8 +413,11 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) - fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) + fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray { + return doSign(findSignatureScheme(schemeCodeName), privateKey, clearData) + } /** * Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature]. @@ -386,11 +429,14 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } + require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" } val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) - if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!") signature.initSign(privateKey) signature.update(clearData) return signature.sign() @@ -398,20 +444,24 @@ object Crypto { /** * Generic way to sign [SignableData] objects with a [PrivateKey]. - * [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as a timestamp or partial and blind signature indicators. - * @param privateKey the signer's [PrivateKey]. + * [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as + * a timestamp or partial and blind signature indicators. + * @param keyPair the signer's [KeyPair]. * @param signableData a [SignableData] object that adds extra information to a transaction. - * @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and the signature metadata. + * @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and + * the signature metadata. * @throws IllegalArgumentException if the signature scheme is not supported for this private key. * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature { val sigKey: SignatureScheme = findSignatureScheme(keyPair.private) val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public) - if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${sigMetaData.schemeCodeName}" + - " is not aligned with the key type: ${sigKey.schemeCodeName}.") + require(sigKey == sigMetaData) { + "Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}." + } val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes) return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata) } @@ -430,8 +480,11 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ + @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) - fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData) + fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { + return doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData) + } /** * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type. @@ -448,8 +501,11 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ - @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) - fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) + fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { + return doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + } /** * Method to verify a digital signature. @@ -465,9 +521,12 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ - @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!") if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!") val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData) @@ -490,14 +549,16 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ - @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean { val signableData = SignableData(txId, transactionSignature.signatureMetadata) return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes) } /** - * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type. + * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the + * input public key's type. * It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature * do not match it returns false rather than throwing an exception. Normally you should use the function which throws, * as it avoids the risk of failing to test the result. @@ -507,14 +568,20 @@ object Crypto { * the passed-in signatureData is improperly encoded or of the wrong type, * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. */ + @JvmStatic @Throws(SignatureException::class) fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean { val signableData = SignableData(txId, transactionSignature.signatureMetadata) - return isValid(findSignatureScheme(transactionSignature.by), transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes) + return isValid( + findSignatureScheme(transactionSignature.by), + transactionSignature.by, + transactionSignature.bytes, + signableData.serialize().bytes) } /** - * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type. + * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the + * input public key's type. * It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature * do not match it returns false rather than throwing an exception. Normally you should use the function which throws, * as it avoids the risk of failing to test the result. @@ -527,8 +594,11 @@ object Crypto { * the passed-in signatureData is improperly encoded or of the wrong type, * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. */ + @JvmStatic @Throws(SignatureException::class) - fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { + return isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + } /** * Method to verify a digital signature. In comparison to [doVerify] if the key and signature @@ -544,9 +614,12 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(SignatureException::class, IllegalArgumentException::class) + @JvmStatic + @Throws(SignatureException::class) fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) signature.initVerify(publicKey) signature.update(clearData) @@ -560,7 +633,7 @@ object Crypto { * @return a KeyPair for the requested signature scheme code name. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName)) /** @@ -570,10 +643,12 @@ object Crypto { * @return a new [KeyPair] for the requested [SignatureScheme]. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(IllegalArgumentException::class) @JvmOverloads + @JvmStatic fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) if (signatureScheme.algSpec != null) keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom()) @@ -638,13 +713,17 @@ object Crypto { * @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. */ + @JvmStatic fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } - when (signatureScheme) { - ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed) - EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed) + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } + return when (signatureScheme) { + ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed) + EDDSA_ED25519_SHA512 -> deriveKeyPairEdDSA(privateKey, seed) + else -> throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is " + + "not currently implemented for ${signatureScheme.schemeCodeName}") } - throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is not currently implemented for ${signatureScheme.schemeCodeName}") } /** @@ -656,6 +735,7 @@ object Crypto { * @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. */ + @JvmStatic fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair { return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed) } @@ -728,11 +808,13 @@ object Crypto { * @return a new [KeyPair] from an entropy input. * @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input. */ + @JvmStatic fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair { - when (signatureScheme) { - EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy) + return when (signatureScheme) { + EDDSA_ED25519_SHA512 -> deriveEdDSAKeyPairFromEntropy(entropy) + else -> throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair " + + "generation: ${signatureScheme.schemeCodeName}") } - throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: ${signatureScheme.schemeCodeName}") } /** @@ -740,6 +822,7 @@ object Crypto { * @param entropy a [BigInteger] value. * @return a new [KeyPair] from an entropy input. */ + @JvmStatic fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) // custom key pair generator from entropy. @@ -766,8 +849,12 @@ object Crypto { } private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter { - override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } - override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } + override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? { + return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } + } + override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? { + return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } + } } /** @@ -784,22 +871,29 @@ object Crypto { * @return true if the point lies on the curve or false if it doesn't. * @throws IllegalArgumentException if the requested signature scheme or the key type is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } - when (publicKey) { - is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid) - is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve) + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } + return when (publicKey) { + is BCECPublicKey -> publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid + is EdDSAPublicKey -> publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}") } } // return true if EdDSA publicKey is point at infinity. // For EdDSA a custom function is required as it is not supported by the I2P implementation. - private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) + private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean { + return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) + } /** Check if the requested [SignatureScheme] is supported by the system. */ - fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme + @JvmStatic + fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean { + return signatureScheme.schemeCodeName in signatureSchemeMap + } // validate a key, by checking its algorithmic params. private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { @@ -812,19 +906,19 @@ object Crypto { // check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean { - when (key) { - is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key) - is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + return when (key) { + is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key) + is BCRSAPublicKey, is BCSphincs256PublicKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") } } // check if a private key satisfies algorithm specs. private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { - when (key) { - is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec - is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec - is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + return when (key) { + is BCECPrivateKey -> key.parameters == signatureScheme.algSpec + is EdDSAPrivateKey -> key.params == signatureScheme.algSpec + is BCRSAPrivateKey, is BCSphincs256PrivateKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") } } @@ -838,21 +932,20 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for a supported key factory to produce a private key. */ - fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey { - return Crypto.decodePublicKey(key.encoded) - } + @JvmStatic + fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey = decodePublicKey(key.encoded) /** * Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. - * This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations. + * This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default + * return SUN implementations. * @param key a public key. * @return a supported implementation of the input public key. * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for a supported key factory to produce a private key. */ - fun toSupportedPublicKey(key: PublicKey): PublicKey { - return Crypto.decodePublicKey(key.encoded) - } + @JvmStatic + fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded) /** * Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. @@ -862,7 +955,6 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for a supported key factory to produce a private key. */ - fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { - return Crypto.decodePrivateKey(key.encoded) - } + @JvmStatic + fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt index f9ca0dad7c..4410171307 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt @@ -42,7 +42,7 @@ class CompositeKey private constructor(val threshold: Int, children: List = Crypto.supportedSignatureSchemes.keys.toList() + val algList: List = Crypto.supportedSignatureSchemes().map { it.schemeCodeName } val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE") assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt index 53ed5580eb..f10261826f 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt @@ -211,7 +211,7 @@ object X509Utilities { nameConstraints: NameConstraints? = null): X509CertificateHolder { val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) - val provider = Crypto.providerMap[signatureScheme.providerName] + val provider = Crypto.findProvider(signatureScheme.providerName) val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) @@ -225,7 +225,7 @@ object X509Utilities { * Create certificate signing request using provided information. */ fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest { - val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.providerMap[signatureScheme.providerName]) + val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName)) return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer) } From dc8d232480de3ad3cb2bf85f5ee32e626aa228bf Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Tue, 15 Aug 2017 18:37:08 +0100 Subject: [PATCH 015/101] Creating a PartyAndCertificate only requires a CertPath --- .../net/corda/core/crypto/X500NameUtils.kt | 3 +- .../net/corda/core/identity/AbstractParty.kt | 3 +- .../net/corda/core/identity/AnonymousParty.kt | 7 +- .../kotlin/net/corda/core/identity/Party.kt | 11 +-- .../core/identity/PartyAndCertificate.kt | 51 +++++------- .../net/corda/core/internal/InternalUtils.kt | 4 + .../kotlin/net/corda/core/node/NodeInfo.kt | 15 ++-- .../core/node/services/IdentityService.kt | 9 ++- .../core/identity/PartyAndCertificateTest.kt | 26 ++++++ .../serialization/DefaultKryoCustomizer.kt | 39 +++++---- .../net/corda/node/internal/AbstractNode.kt | 81 +++++++++---------- .../kotlin/net/corda/node/internal/Node.kt | 5 +- .../identity/InMemoryIdentityService.kt | 27 +++---- .../net/corda/node/services/keys/KMSUtils.kt | 6 +- .../messaging/ArtemisMessagingServer.kt | 12 +-- .../network/PersistentNetworkMapService.kt | 6 +- .../corda/node/utilities/KeyStoreUtilities.kt | 11 ++- .../network/InMemoryIdentityServiceTests.kt | 10 +-- .../NetworkisRegistrationHelperTest.kt | 6 +- .../flow/CommercialPaperIssueFlow.kt | 4 +- .../kotlin/net/corda/testing/CoreTestUtils.kt | 2 +- .../kotlin/net/corda/testing/node/MockNode.kt | 2 +- 22 files changed, 172 insertions(+), 168 deletions(-) create mode 100644 core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt diff --git a/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt index 0043988ba4..6edd82ca93 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt @@ -1,6 +1,7 @@ @file:JvmName("X500NameUtils") package net.corda.core.crypto +import net.corda.core.internal.toX509CertHolder import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500NameBuilder @@ -57,7 +58,7 @@ val X500Name.locationOrNull: String? get() = try { } catch (e: Exception) { null } -val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject +val X509Certificate.subject: X500Name get() = toX509CertHolder().subject val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) /** diff --git a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt index 7dc89ae4a5..b549f33e11 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -13,8 +13,7 @@ import java.security.PublicKey @CordaSerializable abstract class AbstractParty(val owningKey: PublicKey) { /** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */ - override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey - + override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey override fun hashCode(): Int = owningKey.hashCode() abstract fun nameOrNull(): X500Name? diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt index b2264a1c6f..ed6e5b3707 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -1,7 +1,6 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference -import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toStringShort import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name @@ -12,11 +11,7 @@ import java.security.PublicKey * information such as name. It is intended to represent a party on the distributed ledger. */ class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) { - // Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party] - // can put in the key and actual name - override fun toString() = "${owningKey.toStringShort()} " - override fun nameOrNull(): X500Name? = null - override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) + override fun toString() = "Anonymous(${owningKey.toStringShort()})" } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index a2f301be75..5652fc2688 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -1,9 +1,11 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference -import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import java.security.PublicKey /** @@ -26,10 +28,9 @@ import java.security.PublicKey * @see CompositeKey */ class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { - constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public) - override fun toString() = name.toString() - override fun nameOrNull(): X500Name? = name - + constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo)) + override fun nameOrNull(): X500Name = name fun anonymise(): AnonymousParty = AnonymousParty(owningKey) override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) + override fun toString() = name.toString() } diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt index 557c26d7ee..e6f972798a 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -1,49 +1,42 @@ package net.corda.core.identity -import net.corda.core.serialization.CordaSerializable +import net.corda.core.internal.toX509CertHolder import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder import java.security.PublicKey import java.security.cert.* -import java.util.* /** * A full party plus the X.509 certificate and path linking the party back to a trust root. Equality of * [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party, - * not part of the identifier themselves. While party and certificate can both be derived from the certificate path, - * this class exists in order to ensure the implementation classes of certificates and party public keys are kept stable. + * not part of the identifier themselves. */ -@CordaSerializable -data class PartyAndCertificate(val party: Party, - val certificate: X509CertificateHolder, - val certPath: CertPath) { - constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath) - val name: X500Name - get() = party.name - val owningKey: PublicKey - get() = party.owningKey - - override fun equals(other: Any?): Boolean { - return if (other is PartyAndCertificate) - party == other.party - else - false +//TODO Is VerifiableIdentity a better name? +class PartyAndCertificate(val certPath: CertPath) { + @Transient val certificate: X509CertificateHolder + init { + require(certPath.type == "X.509") { "Only X.509 certificates supported" } + val certs = certPath.certificates + require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" } + certificate = certs[0].toX509CertHolder() } + @Transient val party: Party = Party(certificate) + + val owningKey: PublicKey get() = party.owningKey + val name: X500Name get() = party.name + + operator fun component1(): Party = party + operator fun component2(): X509CertificateHolder = certificate + + override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party override fun hashCode(): Int = party.hashCode() override fun toString(): String = party.toString() - /** - * Verify that the given certificate path is valid and leads to the owning key of the party. - */ + /** Verify the certificate path is valid. */ fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult { - require(certPath.certificates.first() is X509Certificate) { "Subject certificate must be an X.509 certificate" } - require(Arrays.equals(party.owningKey.encoded, certificate.subjectPublicKeyInfo.encoded)) { "Certificate public key must match party owning key" } - require(Arrays.equals(certPath.certificates.first().encoded, certificate.encoded)) { "Certificate path must link to certificate" } - - val validatorParameters = PKIXParameters(setOf(trustAnchor)) + val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false } val validator = CertPathValidator.getInstance("PKIX") - validatorParameters.isRevocationEnabled = false - return validator.validate(certPath, validatorParameters) as PKIXCertPathValidatorResult + return validator.validate(certPath, parameters) as PKIXCertPathValidatorResult } } diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index e0d9f37580..b362339540 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 +import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import rx.Observable import rx.Observer @@ -165,6 +166,9 @@ fun logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T } } +fun java.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded) +fun javax.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded) + /** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */ fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash { val bytes = toByteArray() diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 7b71cdcde2..d7f30a4f50 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -24,16 +24,17 @@ data class NodeInfo(val addresses: List, val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services. val legalIdentitiesAndCerts: NonEmptySet, val platformVersion: Int, - var advertisedServices: List = emptyList(), + val advertisedServices: List = emptyList(), val worldMapLocation: WorldMapLocation? = null) { init { - require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" } + require(advertisedServices.none { it.identity == legalIdentityAndCert }) { + "Service identities must be different from node legal identity" + } } - val legalIdentity: Party - get() = legalIdentityAndCert.party - val notaryIdentity: Party - get() = advertisedServices.single { it.info.type.isNotary() }.identity.party + + val legalIdentity: Party get() = legalIdentityAndCert.party + val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party fun serviceIdentities(type: ServiceType): List { - return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party } + return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null } } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 008d114ebc..916ef6dbe6 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -1,7 +1,10 @@ package net.corda.core.node.services import net.corda.core.contracts.PartyAndReference -import net.corda.core.identity.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder import java.security.InvalidAlgorithmParameterException @@ -114,6 +117,6 @@ interface IdentityService { * @param exactMatch If true, a case sensitive match is done against each component of each X.500 name. */ fun partiesFromName(query: String, exactMatch: Boolean): Set - - class UnknownAnonymousPartyException(msg: String) : Exception(msg) } + +class UnknownAnonymousPartyException(msg: String) : Exception(msg) diff --git a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt new file mode 100644 index 0000000000..f6341d9253 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt @@ -0,0 +1,26 @@ +package net.corda.core.identity + +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.testing.getTestPartyAndCertificate +import net.corda.testing.withTestSerialization +import org.assertj.core.api.Assertions.assertThat +import org.bouncycastle.asn1.x500.X500Name +import org.junit.Test +import java.math.BigInteger + +class PartyAndCertificateTest { + @Test + fun `kryo serialisation`() { + withTestSerialization { + val original = getTestPartyAndCertificate(Party( + X500Name("CN=Test Corp,O=Test Corp,L=Madrid,C=ES"), + entropyToKeyPair(BigInteger.valueOf(83)).public)) + val copy = original.serialize().deserialize() + assertThat(copy).isEqualTo(original).isNotSameAs(original) + assertThat(copy.certPath).isEqualTo(original.certPath) + assertThat(copy.certificate).isEqualTo(original.certificate) + } + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index 5112ea6a33..f67d41841d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -12,6 +12,7 @@ import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.guava.* import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializedBytes @@ -59,6 +60,12 @@ object DefaultKryoCustomizer { instantiatorStrategy = CustomInstantiatorStrategy() + // Required for HashCheckingStream (de)serialization. + // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). + addDefaultSerializer(InputStream::class.java, InputStreamSerializer) + addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) + addDefaultSerializer(Logger::class.java, LoggerSerializer) + // WARNING: reordering the registrations here will cause a change in the serialized form, since classes // with custom serializers get written as registration ids. This will break backwards-compatibility. // Please add any new registrations to the end. @@ -68,50 +75,31 @@ object DefaultKryoCustomizer { register(SignedTransaction::class.java, SignedTransactionSerializer) register(WireTransaction::class.java, WireTransactionSerializer) register(SerializedBytes::class.java, SerializedBytesSerializer) - UnmodifiableCollectionsSerializer.registerSerializers(this) ImmutableListSerializer.registerSerializers(this) ImmutableSetSerializer.registerSerializers(this) ImmutableSortedSetSerializer.registerSerializers(this) ImmutableMapSerializer.registerSerializers(this) ImmutableMultimapSerializer.registerSerializers(this) - // InputStream subclasses whitelisting, required for attachments. register(BufferedInputStream::class.java, InputStreamSerializer) register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer) - noReferencesWithin() - register(ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer) register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) - - // Using a custom serializer for compactness - register(CompositeKey::class.java, CompositeKeySerializer) - + register(CompositeKey::class.java, CompositeKeySerializer) // Using a custom serializer for compactness // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. register(Array::class, read = { _, _ -> emptyArray() }, write = { _, _, _ -> }) - // This ensures a NonEmptySetSerializer is constructed with an initial value. register(NonEmptySet::class.java, NonEmptySetSerializer) - - addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) - register(BitSet::class.java, BitSetSerializer()) register(Class::class.java, ClassSerializer) - - addDefaultSerializer(Logger::class.java, LoggerSerializer) - register(FileInputStream::class.java, InputStreamSerializer) - // Required for HashCheckingStream (de)serialization. - // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). - addDefaultSerializer(InputStream::class.java, InputStreamSerializer) - register(CertPath::class.java, CertPathSerializer) register(X509CertPath::class.java, CertPathSerializer) register(X500Name::class.java, X500NameSerializer) register(X509CertificateHolder::class.java, X509CertificateSerializer) - register(BCECPrivateKey::class.java, PrivateKeySerializer) register(BCECPublicKey::class.java, PublicKeySerializer) register(BCRSAPrivateCrtKey::class.java, PrivateKeySerializer) @@ -119,8 +107,8 @@ object DefaultKryoCustomizer { register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer) register(BCSphincs256PublicKey::class.java, PublicKeySerializer) register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) - register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer) + register(PartyAndCertificate::class.java, PartyAndCertificateSerializer) val customization = KryoSerializationCustomization(this) pluginRegistries.forEach { it.customizeSerialization(customization) } @@ -139,6 +127,15 @@ object DefaultKryoCustomizer { } } + private object PartyAndCertificateSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: PartyAndCertificate) { + kryo.writeClassAndObject(output, obj.certPath) + } + override fun read(kryo: Kryo, input: Input, type: Class): PartyAndCertificate { + return PartyAndCertificate(kryo.readClassAndObject(input) as CertPath) + } + } + private object NonEmptySetSerializer : Serializer>() { override fun write(kryo: Kryo, output: Output, obj: NonEmptySet) { // Write out the contents as normal diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index b734859b2c..914be297d7 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -2,6 +2,7 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry import com.google.common.annotations.VisibleForTesting +import com.google.common.collect.Lists import com.google.common.collect.MutableClassToInstanceMap import com.google.common.util.concurrent.MoreExecutors import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner @@ -66,7 +67,6 @@ import net.corda.node.utilities.* import net.corda.node.utilities.AddOrRemove.ADD import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import rx.Observable import java.io.IOException @@ -88,6 +88,9 @@ import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS import java.util.stream.Collectors.toList import kotlin.collections.ArrayList +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair @@ -417,8 +420,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, checkpointStorage = DBCheckpointStorage() _services = ServiceHubInternalImpl() attachments = NodeAttachmentService(configuration.dataSourceProperties, services.monitoringService.metrics, configuration.database) - network = makeMessagingService() - info = makeInfo() + val legalIdentity = obtainIdentity("identity", configuration.myLegalName) + network = makeMessagingService(legalIdentity) + info = makeInfo(legalIdentity) val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.vaultQueryService, services.keyManagementService, services.identityService, platformClock, services.schedulerService) @@ -486,12 +490,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties(), {services.identityService})) } - private fun makeInfo(): NodeInfo { + private fun makeInfo(legalIdentity: PartyAndCertificate): NodeInfo { val advertisedServiceEntries = makeServiceEntries() - val legalIdentity = obtainLegalIdentity() - val allIdentitiesSet = (advertisedServiceEntries.map { it.identity } + legalIdentity).toNonEmptySet() + val allIdentities = (advertisedServiceEntries.map { it.identity } + legalIdentity).toNonEmptySet() val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. - return NodeInfo(addresses, legalIdentity, allIdentitiesSet, platformVersion, advertisedServiceEntries, findMyLocation()) + return NodeInfo(addresses, legalIdentity, allIdentities, platformVersion, advertisedServiceEntries, findMyLocation()) } /** @@ -502,7 +505,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return advertisedServices.map { val serviceId = it.type.id val serviceName = it.name ?: X500Name("${configuration.myLegalName},OU=$serviceId") - val identity = obtainKeyPair(serviceId, serviceName).first + val identity = obtainIdentity(serviceId, serviceName) ServiceEntry(it, identity) } } @@ -613,8 +616,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val instant = platformClock.instant() val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD val reg = NodeRegistration(info, instant.toEpochMilli(), ADD, expires) - val legalIdentityKey = obtainLegalIdentityKey() - val request = RegistrationRequest(reg.toWire(services.keyManagementService, legalIdentityKey.public), network.myAddress) + val request = RegistrationRequest(reg.toWire(services.keyManagementService, info.legalIdentityAndCert.owningKey), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } @@ -680,15 +682,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, runOnStop.clear() } - protected abstract fun makeMessagingService(): MessagingService + protected abstract fun makeMessagingService(legalIdentity: PartyAndCertificate): MessagingService protected abstract fun startMessagingService(rpcOps: RPCOps) - protected fun obtainLegalIdentity(): PartyAndCertificate = identityKeyPair.first - protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second - private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) } - - private fun obtainKeyPair(serviceId: String, serviceName: X500Name): Pair { + private fun obtainIdentity(id: String, name: X500Name): PartyAndCertificate { // Load the private identity key, creating it if necessary. The identity key is a long term well known key that // is distributed to other peers and we use it (or a key signed by it) when we need to do something // "permissioned". The identity file is what gets distributed and contains the node's legal name along with @@ -697,50 +695,52 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // TODO: Integrate with Key management service? val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) - val privateKeyAlias = "$serviceId-private-key" - val compositeKeyAlias = "$serviceId-composite-key" + val privateKeyAlias = "$id-private-key" + val compositeKeyAlias = "$id-composite-key" if (!keyStore.containsAlias(privateKeyAlias)) { val privKeyFile = configuration.baseDirectory / privateKeyAlias - val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" + val pubIdentityFile = configuration.baseDirectory / "$id-public" val compositeKeyFile = configuration.baseDirectory / compositeKeyAlias // TODO: Remove use of [ServiceIdentityGenerator.generateToDisk]. // Get keys from key file. // TODO: this is here to smooth out the key storage transition, remove this migration in future release. if (privKeyFile.exists()) { - migrateKeysFromFile(keyStore, serviceName, pubIdentityFile, privKeyFile, compositeKeyFile, privateKeyAlias, compositeKeyAlias) + migrateKeysFromFile(keyStore, name, pubIdentityFile, privKeyFile, compositeKeyFile, privateKeyAlias, compositeKeyAlias) } else { - log.info("$privateKeyAlias not found in keystore ${configuration.nodeKeystore}, generating fresh key!") - keyStore.saveNewKeyPair(serviceName, privateKeyAlias, generateKeyPair()) + log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") + keyStore.saveNewKeyPair(name, privateKeyAlias, generateKeyPair()) } } - val (cert, keys) = keyStore.certificateAndKeyPair(privateKeyAlias) - // Get keys from keystore. - val loadedServiceName = cert.subject - if (loadedServiceName != serviceName) - throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:$serviceName vs $loadedServiceName") + val (x509Cert, keys) = keyStore.certificateAndKeyPair(privateKeyAlias) - // Use composite key instead if exists // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. - val (keyPair, certs) = if (keyStore.containsAlias(compositeKeyAlias)) { - val compositeKey = Crypto.toSupportedPublicKey(keyStore.getCertificate(compositeKeyAlias).publicKey) - val compositeKeyCert = keyStore.getCertificate(compositeKeyAlias) + val certificates = if (keyStore.containsAlias(compositeKeyAlias)) { + // Use composite key instead if it exists + val certificate = keyStore.getCertificate(compositeKeyAlias) // We have to create the certificate chain for the composite key manually, this is because in order to store - // the chain in keystore we need a private key, however there are no corresponding private key for composite key. - Pair(KeyPair(compositeKey, keys.private), listOf(compositeKeyCert, *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + // the chain in key store we need a private key, however there is no corresponding private key for the composite key. + Lists.asList(certificate, keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)) } else { - Pair(keys, keyStore.getCertificateChain(privateKeyAlias).toList()) + keyStore.getCertificateChain(privateKeyAlias).let { + check(it[0].toX509CertHolder() == x509Cert) { "Certificates from key store do not line up!" } + it.asList() + } } - val certPath = CertificateFactory.getInstance("X509").generateCertPath(certs) + + val subject = certificates[0].toX509CertHolder().subject + if (subject != name) + throw ConfigurationException("The name for $id doesn't match what's in the key store: $name vs $subject") + partyKeys += keys - return Pair(PartyAndCertificate(loadedServiceName, keyPair.public, X509CertificateHolder(certs.first().encoded), certPath), keyPair) + return PartyAndCertificate(CertificateFactory.getInstance("X509").generateCertPath(certificates)) } private fun migrateKeysFromFile(keyStore: KeyStoreWrapper, serviceName: X500Name, pubKeyFile: Path, privKeyFile: Path, compositeKeyFile:Path, privateKeyAlias: String, compositeKeyAlias: String) { - log.info("Migrating $privateKeyAlias from file to keystore...") + log.info("Migrating $privateKeyAlias from file to key store...") // Check that the identity in the config file matches the identity file we have stored to disk. // Load the private key. val publicKey = Crypto.decodePublicKey(pubKeyFile.readAll()) @@ -753,13 +753,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, log.info("Finish migrating $privateKeyAlias from file to keystore.") } - private fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair): PartyAndCertificate { - val certFactory = CertificateFactory.getInstance("X509") - val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) - val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) - return PartyAndCertificate(party, certHolder, certPath) - } - protected open fun generateKeyPair() = cryptoGenerateKeyPair() private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index a00f5ff2d2..7d8fdda207 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -2,6 +2,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import net.corda.core.concurrent.CordaFuture +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture @@ -133,7 +134,7 @@ open class Node(override val configuration: FullNodeConfiguration, private lateinit var userService: RPCUserService - override fun makeMessagingService(): MessagingService { + override fun makeMessagingService(legalIdentity: PartyAndCertificate): MessagingService { userService = RPCUserServiceImpl(configuration.rpcUsers) val (serverAddress, advertisedAddress) = with(configuration) { @@ -147,7 +148,7 @@ open class Node(override val configuration: FullNodeConfiguration, printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) - val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null + val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) legalIdentity.owningKey else null return NodeMessagingClient( configuration, versionInfo, diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index a19ae3698a..80812edd1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -7,7 +7,9 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.toX509CertHolder import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace @@ -16,16 +18,13 @@ import org.bouncycastle.cert.X509CertificateHolder import java.security.InvalidAlgorithmParameterException import java.security.PublicKey import java.security.cert.* -import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.annotation.concurrent.ThreadSafe -import kotlin.collections.LinkedHashSet /** * Simple identity service which caches parties and provides functionality for efficient lookup. * * @param identities initial set of identities for the service, typically only used for unit tests. - * @param certPaths initial set of certificate paths for the service, typically only used for unit tests. */ @ThreadSafe class InMemoryIdentityService(identities: Iterable = emptySet(), @@ -43,7 +42,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS * Certificate store for certificate authority and intermediary certificates. */ override val caCertStore: CertStore - override val trustRootHolder = X509CertificateHolder(trustRoot.encoded) + override val trustRootHolder = trustRoot.toX509CertHolder() override val trustAnchor: TrustAnchor = TrustAnchor(trustRoot, null) private val keyToParties = ConcurrentHashMap() private val principalToParties = ConcurrentHashMap() @@ -54,7 +53,6 @@ class InMemoryIdentityService(identities: Iterable = emptyS keyToParties.putAll(identities.associateBy { it.owningKey } ) principalToParties.putAll(identities.associateBy { it.name }) confidentialIdentities.forEach { identity -> - require(identity.certPath.certificates.size >= 2) { "Certificate path must at least include subject and issuing certificates" } principalToParties.computeIfAbsent(identity.name) { identity } } } @@ -66,13 +64,10 @@ class InMemoryIdentityService(identities: Iterable = emptyS // TODO: Check the certificate validation logic @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { - require(identity.certPath.certificates.size >= 2) { "Certificate path must at least include subject and issuing certificates" } // Validate the chain first, before we do anything clever with it identity.verify(trustAnchor) log.trace { "Registering identity $identity" } - require(Arrays.equals(identity.certificate.subjectPublicKeyInfo.encoded, identity.owningKey.encoded)) { "Party certificate must end with party's public key" } - keyToParties[identity.owningKey] = identity // Always keep the first party we registered, as that's the well known identity principalToParties.computeIfAbsent(identity.name) { identity } @@ -83,7 +78,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS override fun certificateFromParty(party: Party): PartyAndCertificate = principalToParties[party.name] ?: throw IllegalArgumentException("Unknown identity ${party.name}") // We give the caller a copy of the data set to avoid any locking problems - override fun getAllIdentities(): Iterable = java.util.ArrayList(keyToParties.values) + override fun getAllIdentities(): Iterable = ArrayList(keyToParties.values) override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]?.party override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal]?.party @@ -128,13 +123,13 @@ class InMemoryIdentityService(identities: Iterable = emptyS return results } - @Throws(IdentityService.UnknownAnonymousPartyException::class) + @Throws(UnknownAnonymousPartyException::class) override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) { - val path = keyToParties[anonymousParty.owningKey]?.certPath ?: throw IdentityService.UnknownAnonymousPartyException("Unknown anonymous party ${anonymousParty.owningKey.toStringShort()}") - require(path.certificates.size > 1) { "Certificate path must contain at least two certificates" } - val actual = path.certificates[1] - require(actual is X509Certificate && actual.publicKey == party.owningKey) { "Next certificate in the path must match the party key ${party.owningKey.toStringShort()}." } - val target = path.certificates.first() - require(target is X509Certificate && target.publicKey == anonymousParty.owningKey) { "Certificate path starts with a certificate for the anonymous party" } + val anonymousIdentity = keyToParties[anonymousParty.owningKey] ?: + throw UnknownAnonymousPartyException("Unknown $anonymousParty") + val issuingCert = anonymousIdentity.certPath.certificates[1] + require(issuingCert.publicKey == party.owningKey) { + "Issuing certificate's public key must match the party key ${party.owningKey.toStringShort()}." + } } } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 07bf6b363b..36a2c4f34d 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -3,7 +3,6 @@ package net.corda.node.services.keys import net.corda.core.crypto.ContentSignerBuilder import net.corda.core.crypto.Crypto import net.corda.core.crypto.cert -import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.utilities.days @@ -35,10 +34,11 @@ fun freshCertificate(identityService: IdentityService, revocationEnabled: Boolean = false): PartyAndCertificate { val issuerCertificate = issuer.certificate val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCertificate) - val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) + val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, + issuerSigner, issuer.name, subjectPublicKey, window) val certFactory = CertificateFactory.getInstance("X509") val ourCertPath = certFactory.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) - val anonymisedIdentity = PartyAndCertificate(Party(issuer.name, subjectPublicKey), ourCertificate, ourCertPath) + val anonymisedIdentity = PartyAndCertificate(ourCertPath) identityService.verifyAndRegisterIdentity(anonymisedIdentity) return anonymisedIdentity } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 919a744191..829c36bfbf 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle +import net.corda.core.internal.toX509CertHolder import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange @@ -25,7 +26,6 @@ import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_TLS import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA -import net.corda.node.utilities.getX509Certificate import net.corda.node.utilities.loadKeyStore import net.corda.nodeapi.* import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER @@ -52,7 +52,6 @@ import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal import org.apache.activemq.artemis.utils.ConfigurationHelper import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder import rx.Subscription import java.io.IOException import java.math.BigInteger @@ -273,12 +272,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { val keyStore = loadKeyStore(config.sslKeystore, config.keyStorePassword) val trustStore = loadKeyStore(config.trustStoreFile, config.trustStorePassword) - val ourCertificate = keyStore.getX509Certificate(CORDA_CLIENT_TLS) - // This is a sanity check and should not fail unless things have been misconfigured - require(ourCertificate.subject == config.myLegalName) { - "Legal name does not match with our subject CN: ${ourCertificate.subject}" - } val defaultCertPolicies = mapOf( PEER_ROLE to CertificateChainCheckPolicy.RootMustMatch, NODE_ROLE to CertificateChainCheckPolicy.LeafMustMatch, @@ -512,12 +506,12 @@ private class VerifyingNettyConnector(configuration: MutableMap, "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" } // Make sure certificate has the same name. - val peerCertificate = X509CertificateHolder(session.peerCertificateChain.first().encoded) + val peerCertificate = session.peerCertificateChain[0].toX509CertHolder() require(peerCertificate.subject == expectedLegalName) { "Peer has wrong subject name in the certificate - expected $expectedLegalName but got ${peerCertificate.subject}. This is either a fatal " + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" } - X509Utilities.validateCertificateChain(X509CertificateHolder(session.localCertificates.last().encoded), *session.peerCertificates) + X509Utilities.validateCertificateChain(session.localCertificates.last().toX509CertHolder(), *session.peerCertificates) server.onTcpConnection(peerLegalName) } catch (e: IllegalArgumentException) { connection.close() diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 1cb7de5292..f8f340f817 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -21,6 +21,7 @@ import java.util.Collections.synchronizedMap class PersistentNetworkMapService(services: ServiceHubInternal, minimumPlatformVersion: Int) : AbstractNetworkMapService(services, minimumPlatformVersion) { + // Only the node_party_path column is needed to reconstruct a PartyAndCertificate but we have the others for human readability private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}network_map_nodes") { val nodeParty = partyAndCertificate("node_party_name", "node_party_key", "node_party_certificate", "node_party_path") val registrationInfo = blob("node_registration_info") @@ -28,16 +29,15 @@ class PersistentNetworkMapService(services: ServiceHubInternal, minimumPlatformV override val nodeRegistrations: MutableMap = synchronizedMap(object : AbstractJDBCHashMap(Table, loadOnInit = true) { // TODO: We should understand an X500Name database field type, rather than manually doing the conversion ourselves - override fun keyFromRow(row: ResultRow): PartyAndCertificate = PartyAndCertificate(X500Name(row[table.nodeParty.name]), row[table.nodeParty.owningKey], - row[table.nodeParty.certificate], row[table.nodeParty.certPath]) + override fun keyFromRow(row: ResultRow): PartyAndCertificate = PartyAndCertificate(row[table.nodeParty.certPath]) override fun valueFromRow(row: ResultRow): NodeRegistrationInfo = deserializeFromBlob(row[table.registrationInfo]) override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { insert[table.nodeParty.name] = entry.key.name.toString() insert[table.nodeParty.owningKey] = entry.key.owningKey - insert[table.nodeParty.certPath] = entry.key.certPath insert[table.nodeParty.certificate] = entry.key.certificate + insert[table.nodeParty.certPath] = entry.key.certPath } override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { diff --git a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt index b429f2e09e..fbe2947a70 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt @@ -1,8 +1,11 @@ package net.corda.node.utilities -import net.corda.core.crypto.* +import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.cert import net.corda.core.internal.exists import net.corda.core.internal.read +import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.write import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder @@ -145,8 +148,8 @@ fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): Certi * @return The X509Certificate found in the KeyStore under the specified alias. */ fun KeyStore.getX509Certificate(alias: String): X509CertificateHolder { - val encoded = getCertificate(alias)?.encoded ?: throw IllegalArgumentException("No certificate under alias \"$alias\"") - return X509CertificateHolder(encoded) + val certificate = getCertificate(alias) ?: throw IllegalArgumentException("No certificate under alias \"$alias\"") + return certificate.toX509CertHolder() } /** @@ -206,5 +209,5 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias) - fun certificateAndKeyPair(alias: String) = keyStore.getCertificateAndKeyPair(alias, storePassword) + fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword) } diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index 4507bd395a..c7809739fb 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -7,7 +7,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities @@ -90,10 +90,10 @@ class InMemoryIdentityServiceTests { val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) // TODO: Generate certificate with an EdDSA key rather than ECDSA - val identity = Party(CertificateAndKeyPair(rootCert, rootKey)) + val identity = Party(rootCert) val txIdentity = AnonymousParty(txKey.public) - assertFailsWith { + assertFailsWith { service.assertOwnership(identity, txIdentity) } } @@ -107,7 +107,7 @@ class InMemoryIdentityServiceTests { fun `get anonymous identity by key`() { val trustRoot = DUMMY_CA val (alice, aliceTxIdentity) = createParty(ALICE.name, trustRoot) - val (bob, bobTxIdentity) = createParty(ALICE.name, trustRoot) + val (_, bobTxIdentity) = createParty(ALICE.name, trustRoot) // Now we have identities, construct the service and let it know about both val service = InMemoryIdentityService(setOf(alice), emptySet(), trustRoot.certificate.cert) @@ -163,7 +163,7 @@ class InMemoryIdentityServiceTests { val txKey = Crypto.generateKeyPair() val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public) val txCertPath = certFactory.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) - return Pair(issuer, PartyAndCertificate(Party(x500Name, txKey.public), txCert, txCertPath)) + return Pair(issuer, PartyAndCertificate(txCertPath)) } /** diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index 51de115d1f..da5569e280 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -9,12 +9,12 @@ import net.corda.core.crypto.cert import net.corda.core.crypto.commonName import net.corda.core.internal.exists import net.corda.core.internal.toTypedArray +import net.corda.core.internal.toX509CertHolder import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.loadKeyStore import net.corda.testing.ALICE import net.corda.testing.getTestX509Name import net.corda.testing.testNodeConfiguration -import org.bouncycastle.cert.X509CertificateHolder import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -68,7 +68,7 @@ class NetworkRegistrationHelperTest { assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) assertEquals(3, certificateChain.size) - assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { it.toX509CertHolder().subject.commonName }) } sslKeystore.run { @@ -78,7 +78,7 @@ class NetworkRegistrationHelperTest { assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) assertEquals(4, certificateChain.size) - assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { it.toX509CertHolder().subject.commonName }) } trustStore.run { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt index 2df13766d3..ba813d6df3 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/CommercialPaperIssueFlow.kt @@ -3,11 +3,9 @@ package net.corda.traderdemo.flow import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.core.contracts.Amount -import net.corda.finance.`issued by` import net.corda.core.crypto.SecureHash import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction @@ -16,13 +14,13 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.days import net.corda.core.utilities.seconds +import net.corda.finance.`issued by` import java.time.Instant import java.util.* /** * Flow for the Bank of Corda node to issue some commercial paper to the seller's node, to sell to the buyer. */ -@InitiatingFlow @StartableByRPC class CommercialPaperIssueFlow(val amount: Amount, val issueRef: OpaqueBytes, diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index ed759632be..663fe30dcf 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -207,7 +207,7 @@ fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = val certFactory = CertificateFactory.getInstance("X509") val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) - return PartyAndCertificate(party, certHolder, certPath) + return PartyAndCertificate(certPath) } /** diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index af092dd5f0..603de51d4c 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -149,7 +149,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // We only need to override the messaging service here, as currently everything that hits disk does so // through the java.nio API which we are already mocking via Jimfs. - override fun makeMessagingService(): MessagingService { + override fun makeMessagingService(legalIdentity: PartyAndCertificate): MessagingService { require(id >= 0) { "Node ID must be zero or positive, was passed: " + id } return mockNet.messagingNetwork.createNodeWithID( !mockNet.threadPerNode, From ca5ea1e25d9128efe2d592781bbe1f93f01a4633 Mon Sep 17 00:00:00 2001 From: Matthew Nesbit Date: Wed, 16 Aug 2017 17:44:29 +0100 Subject: [PATCH 016/101] Fix a bug where when using a groovy string a cordapp will not be included because of a string and GString equality failure. --- constants.properties | 2 +- .../src/main/groovy/net/corda/plugins/Cordformation.groovy | 2 +- .../cordformation/src/main/groovy/net/corda/plugins/Node.groovy | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/constants.properties b/constants.properties index 4567dcffbb..b6155a12a4 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.13.6 +gradlePluginsVersion=0.15.0 kotlinVersion=1.1.1 guavaVersion=21.0 bouncycastleVersion=1.57 diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index edd74182f2..8b0076cfb2 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -76,7 +76,7 @@ class Cordformation implements Plugin { "This can cause node stability problems. Please use 'corda' instead." + "See http://docs.corda.net/cordapp-build-systems.html") } else { - logger.trace("Including dependency: $it") + logger.info("Including dependency: $it") } } return filteredDeps.collect { configurations.runtime.files it }.flatten().toSet() diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index fb2ea88550..f530928695 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -235,6 +235,8 @@ class Node extends CordformNode { * @return List of this node's cordapps. */ private Collection getCordappList() { + // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string + List cordapps = this.cordapps.collect { it.toString() } return project.configurations.cordapp.files { cordapps.contains(it.group + ":" + it.name + ":" + it.version) } From 1b61f94bb8e6e3d3c5fbdf218439ab978aa5c347 Mon Sep 17 00:00:00 2001 From: Clinton Alexander Date: Wed, 16 Aug 2017 18:04:41 +0100 Subject: [PATCH 017/101] Improved logging message for including cordapp dependencies. --- .../src/main/groovy/net/corda/plugins/Cordformation.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index 8b0076cfb2..59b495879e 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -76,7 +76,7 @@ class Cordformation implements Plugin { "This can cause node stability problems. Please use 'corda' instead." + "See http://docs.corda.net/cordapp-build-systems.html") } else { - logger.info("Including dependency: $it") + logger.info("Including dependency in CorDapp JAR: $it") } } return filteredDeps.collect { configurations.runtime.files it }.flatten().toSet() From 48e8aa55faf39263fe62bbfd87d4537b80862512 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 17 Aug 2017 09:30:27 +0100 Subject: [PATCH 018/101] Vault identity cleanup (#1194) * Removed notary_key (and all references) from vault schema. Fixed incorrect NOTARY usage in certain tests (cash consumption) * Fixed broken test. * Replace CommonSchemaV1.Party in all VaultSchema tables (and associated queries) with string'ified X500Name's only. * Fix broken tests. * Completely remove CommonSchemaV1.Party and all references (in favour of X500Name's) * Updated all schema attribute identity references to use AbstractParty. * Updated all schema attribute identity references to use AbstractParty. * Standarised attribute naming for parties (removed 'Name') * Updated deprecate identity API references following rebase. * Configurable IdentityService as a lambda in JUnit tests. * Additional WARNING logging to enable troubleshooting of identity lookup failures. * Final identity updates to sample schemas. Cleaned up several compiler warnings. --- .../corda/core/node/services/VaultService.kt | 4 +- .../core/node/services/vault/QueryCriteria.kt | 4 +- .../node/services/vault/QueryCriteriaUtils.kt | 2 +- .../net/corda/core/schemas/CommonSchema.kt | 67 ++++++--------- ...bstractPartyToX500NameAsStringConverter.kt | 12 ++- .../core/flows/ContractUpgradeFlowTest.kt | 7 +- .../corda/docs/FxTransactionBuildTutorial.kt | 5 +- .../kotlin/net/corda/contracts/asset/Cash.kt | 5 +- .../kotlin/net/corda/schemas/CashSchemaV1.kt | 6 +- .../corda/contracts/DummyFungibleContract.kt | 14 ++-- .../net/corda/schemas/SampleCashSchemaV2.kt | 4 +- .../net/corda/schemas/SampleCashSchemaV3.kt | 28 +++---- .../schemas/SampleCommercialPaperSchemaV2.kt | 4 +- .../vault/schemas/requery/VaultSchema.kt | 3 - .../services/vault/schemas/VaultSchemaTest.kt | 4 - .../vault/HibernateQueryCriteriaParser.kt | 31 +++---- .../services/vault/HibernateVaultQueryImpl.kt | 9 +- .../node/services/vault/NodeVaultService.kt | 1 - .../corda/node/services/vault/VaultSchema.kt | 43 ++++++---- .../services/vault/VaultQueryJavaTests.java | 9 +- .../database/HibernateConfigurationTest.kt | 11 ++- .../database/RequeryConfigurationTest.kt | 1 - .../services/vault/NodeVaultServiceTest.kt | 6 +- .../node/services/vault/VaultQueryTests.kt | 82 +++++++++++++++---- .../kotlin/net/corda/testing/CoreTestUtils.kt | 5 +- .../kotlin/net/corda/testing/TestConstants.kt | 1 + .../testing/contracts/DummyDealContract.kt | 1 + .../testing/contracts/DummyLinearContract.kt | 2 + .../corda/testing/contracts/VaultFiller.kt | 6 +- .../net/corda/testing/node/MockServices.kt | 8 +- .../testing/schemas/DummyDealStateSchemaV1.kt | 6 +- .../schemas/DummyLinearStateSchemaV1.kt | 13 ++- .../schemas/DummyLinearStateSchemaV2.kt | 6 +- 33 files changed, 237 insertions(+), 173 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 183557f355..ebd70ae93d 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException +import net.corda.core.identity.AbstractParty import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture @@ -135,8 +136,7 @@ class Vault(val states: Iterable>) { val recordedTime: Instant, val consumedTime: Instant?, val status: Vault.StateStatus, - val notaryName: String, - val notaryKey: String, + val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?) } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 0be25265f6..910bd33b12 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -51,7 +51,7 @@ sealed class QueryCriteria { data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, val contractStateTypes: Set>? = null, val stateRefs: List? = null, - val notaryName: List? = null, + val notary: List? = null, val softLockingCondition: SoftLockingCondition? = null, val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { @@ -81,7 +81,7 @@ sealed class QueryCriteria { data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, val owner: List? = null, val quantity: ColumnPredicate? = null, - val issuerPartyName: List? = null, + val issuer: List? = null, val issuerRef: List? = null, override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 88364f3bb2..51cbcde689 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -151,7 +151,7 @@ data class Sort(val columns: Collection) { enum class VaultStateAttribute(val attributeName: String) : Attribute { /** Vault States */ - NOTARY_NAME("notaryName"), + NOTARY_NAME("notary"), CONTRACT_TYPE("contractStateClassName"), STATE_STATUS("stateStatus"), RECORDED_TIME("recordedTime"), diff --git a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt index a74001d912..5a55d9cc92 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -17,10 +17,17 @@ object CommonSchema /** * First version of the Vault ORM schema */ -object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = listOf(Party::class.java)) { +object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = emptyList()) { @MappedSuperclass open class LinearState( + /** [ContractState] attributes */ + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, + /** * Represents a [LinearState] [UniqueIdentifier] */ @@ -31,18 +38,26 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers var uuid: UUID ) : PersistentState() { - constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id) + constructor(uid: UniqueIdentifier, _participants: Set) + : this(participants = _participants.toMutableSet(), + externalId = uid.externalId, + uuid = uid.id) } @MappedSuperclass open class FungibleState( /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, /** [OwnableState] attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var ownerKey: CommonSchemaV1.Party, + + /** X500Name of owner party **/ + @Column(name = "owner_name") + var owner: AbstractParty, /** [FungibleAsset] attributes * @@ -55,42 +70,12 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers var quantity: Long, /** Issuer attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var issuerParty: CommonSchemaV1.Party, + + /** X500Name of issuer party **/ + @Column(name = "issuer_name") + var issuer: AbstractParty, @Column(name = "issuer_reference") var issuerRef: ByteArray - ) : PersistentState() { - constructor(_participants: Set, _ownerKey: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: ByteArray) - : this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(), - ownerKey = CommonSchemaV1.Party(_ownerKey), - quantity = _quantity, - issuerParty = CommonSchemaV1.Party(_issuerParty), - issuerRef = _issuerRef) - } - - /** - * Party entity (to be replaced by referencing final Identity Schema) - */ - @Entity - @Table(name = "vault_party", - indexes = arrayOf(Index(name = "party_name_idx", columnList = "party_name"))) - class Party( - @Id - @GeneratedValue - @Column(name = "party_id") - var id: Int, - - /** - * [Party] attributes - */ - @Column(name = "party_name") - var name: String, - - @Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?) - var key: String - ) { - constructor(party: AbstractParty) - : this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String()) - } + ) : PersistentState() } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt index 9ee9503eef..056eb3becf 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt @@ -2,6 +2,7 @@ package net.corda.core.schemas.converters import net.corda.core.identity.AbstractParty import net.corda.core.node.services.IdentityService +import net.corda.core.utilities.loggerFor import org.bouncycastle.asn1.x500.X500Name import javax.persistence.AttributeConverter import javax.persistence.Converter @@ -17,9 +18,15 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic identitySvc() } + companion object { + val log = loggerFor() + } + override fun convertToDatabaseColumn(party: AbstractParty?): String? { party?.let { - return identityService.partyFromAnonymous(party)?.toString() + val partyName = identityService.partyFromAnonymous(party)?.toString() + if (partyName != null) return partyName + else log.warn ("Identity service unable to resolve AbstractParty: $party") } return null // non resolvable anonymous parties } @@ -27,7 +34,8 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic override fun convertToEntityAttribute(dbData: String?): AbstractParty? { dbData?.let { val party = identityService.partyFromX500Name(X500Name(dbData)) - return party as AbstractParty + if (party != null) return party + else log.warn ("Identity service unable to resolve X500name: $dbData") } return null // non resolvable anonymous parties are stored as nulls } diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 3d9b30e76d..cc6e06dba6 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -44,11 +44,14 @@ class ContractUpgradeFlowTest { @Before fun setup() { mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes() + val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override a = nodes.partyNodes[0] b = nodes.partyNodes[1] notary = nodes.notaryNode.info.notaryIdentity - mockNet.runNetwork() + + val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == nodes.notaryNode.info.notaryIdentity } + a.services.identityService.registerIdentity(nodeIdentity) + b.services.identityService.registerIdentity(nodeIdentity) } @After diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 14d0efb9fa..f6b2760300 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -12,6 +12,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.* +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.vault.QueryCriteria @@ -42,8 +43,8 @@ private fun gatherOurInputs(serviceHub: ServiceHub, val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") } val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties) - val notaryName = if (notary != null) notary.name else serviceHub.networkMapCache.getAnyNotary()!!.name - val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = listOf(notaryName)) + val notaries = notary ?: serviceHub.networkMapCache.getAnyNotary() + val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = listOf(notaries as AbstractParty)) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) } val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 02832f6893..7d4887cc5a 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -10,6 +10,7 @@ import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.Emoji import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException @@ -98,7 +99,7 @@ class Cash : OnLedgerAsset() { override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is CashSchemaV1 -> CashSchemaV1.PersistentCashState( - owner = this.owner.owningKey.toBase58String(), + owner = this.owner, pennies = this.amount.quantity, currency = this.amount.token.product.currencyCode, issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(), @@ -323,7 +324,7 @@ class Cash : OnLedgerAsset() { AND (vs.lock_id = '$lockId' OR vs.lock_id is null) """ + (if (notary != null) - " AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") + + " AND vs.notary_name = '${notary.name}'" else "") + (if (onlyFromIssuerParties.isNotEmpty()) " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + (if (withIssuerRefs.isNotEmpty()) diff --git a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt index 8e11e1f8c8..78fe0b2b90 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt @@ -1,5 +1,6 @@ package net.corda.schemas +import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable @@ -21,8 +22,9 @@ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( - @Column(name = "owner_key") - var owner: String, + /** X500Name of owner party **/ + @Column(name = "owner_name") + var owner: AbstractParty, @Column(name = "pennies") var pennies: Long, diff --git a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt index 401b8ae324..4432189ee2 100644 --- a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt +++ b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt @@ -54,7 +54,7 @@ class DummyFungibleContract : OnLedgerAsset SampleCashSchemaV2.PersistentCashState( - _participants = this.participants.toSet(), + _participants = this.participants.toMutableSet(), _owner = this.owner, _quantity = this.amount.quantity, currency = this.amount.token.product.currencyCode, @@ -62,12 +62,12 @@ class DummyFungibleContract : OnLedgerAsset SampleCashSchemaV3.PersistentCashState( - _participants = this.participants.toSet(), - _owner = this.owner, - _quantity = this.amount.quantity, - _currency = this.amount.token.product.currencyCode, - _issuerParty = this.amount.token.issuer.party, - _issuerRef = this.amount.token.issuer.reference.bytes + participants = this.participants.toMutableSet(), + owner = this.owner, + pennies = this.amount.quantity, + currency = this.amount.token.product.currencyCode, + issuer = this.amount.token.issuer.party, + issuerRef = this.amount.token.issuer.reference.bytes ) else -> throw IllegalArgumentException("Unrecognised schema $schema") } diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt index 83755a4110..5732011e4a 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt @@ -13,7 +13,7 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 2, - mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) { + mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "cash_states_v2", indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code"))) @@ -33,5 +33,5 @@ object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, ve val _issuerParty: AbstractParty, @Transient val _issuerRef: ByteArray - ) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef) + ) : CommonSchemaV1.FungibleState(_participants.toMutableSet(), _owner, _quantity, _issuerParty, _issuerRef) } diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt index d1d7e46d79..50ed78ab59 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt @@ -11,16 +11,19 @@ import javax.persistence.* * at the time of writing. */ object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 3, - mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) { + mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "cash_states_v3") class PersistentCashState( /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var owner: CommonSchemaV1.Party, + /** X500Name of participant parties **/ + @ElementCollection + var participants: MutableSet? = null, + + /** X500Name of owner party **/ + @Column(name = "owner_name") + var owner: AbstractParty, @Column(name = "pennies") var pennies: Long, @@ -28,18 +31,11 @@ object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, ve @Column(name = "ccy_code", length = 3) var currency: String, - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var issuerParty: CommonSchemaV1.Party, + /** X500Name of issuer party **/ + @Column(name = "issuer_name") + var issuer: AbstractParty, @Column(name = "issuer_ref") var issuerRef: ByteArray - ) : PersistentState() { - constructor(_participants: Set, _owner: AbstractParty, _quantity: Long, _currency: String, _issuerParty: AbstractParty, _issuerRef: ByteArray) - : this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(), - owner = CommonSchemaV1.Party(_owner), - pennies = _quantity, - currency = _currency, - issuerParty = CommonSchemaV1.Party(_issuerParty), - issuerRef = _issuerRef) - } + ) : PersistentState() } diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt index 735eda7afc..118b10d766 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt @@ -14,7 +14,7 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, - mappedTypes = listOf(PersistentCommercialPaperState::class.java, CommonSchemaV1.Party::class.java)) { + mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states_v2", indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"), @@ -44,5 +44,5 @@ object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPap val _issuerParty: AbstractParty, @Transient val _issuerRef: ByteArray - ) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef) + ) : CommonSchemaV1.FungibleState(_participants.toMutableSet(), _owner, _quantity, _issuerParty, _issuerRef) } diff --git a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt b/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt index a32d12ef23..34130eb7c7 100644 --- a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt +++ b/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt @@ -41,9 +41,6 @@ object VaultSchema { @get:Column(name = "notary_name") var notaryName: String - @get:Column(name = "notary_key", length = 65535) // TODO What is the upper limit on size of CompositeKey? - var notaryKey: String - /** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */ @get:Column(name = "contract_state_class_name") var contractStateClassName: String diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index 554353c29e..97d54aa094 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -314,7 +314,6 @@ class VaultSchemaTest : TestDependencyInjectionBase() { contractStateClassName = state.data.javaClass.name contractState = state.serialize().bytes notaryName = state.notary.name.toString() - notaryKey = state.notary.owningKey.toBase58String() recordedTime = Instant.now() } } @@ -655,15 +654,12 @@ class VaultSchemaTest : TestDependencyInjectionBase() { @Test fun insertWithBigCompositeKey() { - val keys = (1..314).map { generateKeyPair().public } - val bigNotaryKey = CompositeKey.Builder().addKeys(keys).build() val vaultStEntity = VaultStatesEntity().apply { txId = SecureHash.randomSHA256().toString() index = 314 stateStatus = Vault.StateStatus.UNCONSUMED contractStateClassName = VaultNoopContract.VaultNoopState::class.java.name notaryName = "Huge distributed notary" - notaryKey = bigNotaryKey.toBase58String() recordedTime = Instant.now() } data.insert(vaultStEntity) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 114fc8b792..db3cbe47f6 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -7,14 +7,12 @@ import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria -import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toHexString import net.corda.core.utilities.trace -import org.bouncycastle.asn1.x500.X500Name import java.util.* import javax.persistence.Tuple import javax.persistence.criteria.* @@ -68,9 +66,8 @@ class HibernateQueryCriteriaParser(val contractType: Class, } // notary names - criteria.notaryName?.let { - val notaryNames = (criteria.notaryName as List).map { it.toString() } - predicateSet.add(criteriaBuilder.and(vaultStates.get("notaryName").`in`(notaryNames))) + criteria.notary?.let { + predicateSet.add(criteriaBuilder.and(vaultStates.get("notary").`in`(criteria.notary))) } // state references @@ -243,10 +240,8 @@ class HibernateQueryCriteriaParser(val contractType: Class, // owner criteria.owner?.let { - val ownerKeys = criteria.owner as List - val joinFungibleStateToParty = vaultFungibleStates.join("issuerParty") - val owners = ownerKeys.map { it.nameOrNull()?.toString() ?: it.toString()} - predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(owners))) + val owners = criteria.owner as List + predicateSet.add(criteriaBuilder.and(vaultFungibleStates.get("owner").`in`(owners))) } // quantity @@ -255,11 +250,9 @@ class HibernateQueryCriteriaParser(val contractType: Class, } // issuer party - criteria.issuerPartyName?.let { - val issuerParties = criteria.issuerPartyName as List - val joinFungibleStateToParty = vaultFungibleStates.join("issuerParty") - val issuerPartyNames = issuerParties.map { it.nameOrNull().toString() } - predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(issuerPartyNames))) + criteria.issuer?.let { + val issuerParties = criteria.issuer as List + predicateSet.add(criteriaBuilder.and(vaultFungibleStates.get("issuer").`in`(issuerParties))) } // issuer reference @@ -271,9 +264,8 @@ class HibernateQueryCriteriaParser(val contractType: Class, // participants criteria.participants?.let { val participants = criteria.participants as List - val joinFungibleStateToParty = vaultFungibleStates.join("participants") - val participantKeys = participants.map { it.nameOrNull().toString() } - predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(participantKeys))) + val joinLinearStateToParty = vaultFungibleStates.joinSet("participants") + predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) criteriaQuery.distinct(true) } return predicateSet @@ -310,9 +302,8 @@ class HibernateQueryCriteriaParser(val contractType: Class, // deal participants criteria.participants?.let { val participants = criteria.participants as List - val joinLinearStateToParty = vaultLinearStates.join("participants") - val participantKeys = participants.map { it.nameOrNull().toString() } - predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.get("name").`in`(participantKeys))) + val joinLinearStateToParty = vaultLinearStates.joinSet("participants") + predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) criteriaQuery.distinct(true) } return predicateSet diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index 1b0156364b..a8a58bd2a8 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -119,7 +119,14 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val vaultState = result[0] as VaultSchemaV1.VaultStates val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) val state = vaultState.contractState.deserialize>(context = STORAGE_CONTEXT) - statesMeta.add(Vault.StateMetadata(stateRef, vaultState.contractStateClassName, vaultState.recordedTime, vaultState.consumedTime, vaultState.stateStatus, vaultState.notaryName, vaultState.notaryKey, vaultState.lockId, vaultState.lockUpdateTime)) + statesMeta.add(Vault.StateMetadata(stateRef, + vaultState.contractStateClassName, + vaultState.recordedTime, + vaultState.consumedTime, + vaultState.stateStatus, + vaultState.notary, + vaultState.lockId, + vaultState.lockUpdateTime)) statesAndRefs.add(StateAndRef(state, stateRef)) } else { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 04e98b5e75..88eb992172 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -98,7 +98,6 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P contractStateClassName = it.value.state.data.javaClass.name contractState = it.value.state.serialize(context = STORAGE_CONTEXT).bytes notaryName = it.value.state.notary.name.toString() - notaryKey = it.value.state.notary.owningKey.toBase58String() recordedTime = services.clock.instant() } insert(state) diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 9ead62d2c9..1d0d5a80a2 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -1,9 +1,9 @@ package net.corda.node.services.vault +import net.corda.core.contracts.ContractState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault -import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable @@ -22,17 +22,14 @@ object VaultSchema */ @CordaSerializable object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1, - mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, CommonSchemaV1.Party::class.java)) { + mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java)) { @Entity @Table(name = "vault_states", indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status"))) class VaultStates( - /** refers to the notary a state is attached to */ + /** refers to the X500Name of the notary a state is attached to */ @Column(name = "notary_name") - var notaryName: String, - - @Column(name = "notary_key", length = 65535) // TODO What is the upper limit on size of CompositeKey? - var notaryKey: String, + var notary: AbstractParty, /** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */ @Column(name = "contract_state_class_name") @@ -71,8 +68,13 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio Index(name = "uuid_index", columnList = "uuid"))) class VaultLinearStates( /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, + // Reason for not using Set is described here: + // https://stackoverflow.com/questions/44213074/kotlin-collection-has-neither-generic-type-or-onetomany-targetentity /** * Represents a [LinearState] [UniqueIdentifier] @@ -86,18 +88,23 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio constructor(uid: UniqueIdentifier, _participants: List) : this(externalId = uid.externalId, uuid = uid.id, - participants = _participants.map{ CommonSchemaV1.Party(it) }.toSet() ) + participants = _participants.toMutableSet()) } @Entity @Table(name = "vault_fungible_states") class VaultFungibleStates( /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, /** [OwnableState] attributes */ - @Column(name = "owner_id") + + /** X500Name of owner party **/ + @Column(name = "owner_name") var owner: AbstractParty, /** [FungibleAsset] attributes @@ -111,8 +118,10 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio var quantity: Long, /** Issuer attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var issuerParty: CommonSchemaV1.Party, + + /** X500Name of issuer party **/ + @Column(name = "issuer_name") + var issuer: AbstractParty, @Column(name = "issuer_reference") var issuerRef: ByteArray @@ -120,8 +129,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : this(owner = _owner, quantity = _quantity, - issuerParty = CommonSchemaV1.Party(_issuerParty), + issuer = _issuerParty, issuerRef = _issuerRef.bytes, - participants = _participants.map { CommonSchemaV1.Party(it) }.toSet()) + participants = _participants.toMutableSet()) } } \ No newline at end of file diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 1d2cbb9995..f779d62273 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -8,9 +8,7 @@ import net.corda.core.contracts.*; import net.corda.core.crypto.EncodingUtils; import net.corda.core.identity.AbstractParty; import net.corda.core.messaging.DataFeed; -import net.corda.core.node.services.Vault; -import net.corda.core.node.services.VaultQueryException; -import net.corda.core.node.services.VaultQueryService; +import net.corda.core.node.services.*; import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; @@ -44,6 +42,7 @@ import static net.corda.core.utilities.ByteArrays.toHexString; import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.TestConstants.*; import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices; +import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests extends TestDependencyInjectionBase { @@ -58,7 +57,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); keys.add(getDUMMY_NOTARY_KEY()); - Pair databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys); + + IdentityService identitySvc = makeTestIdentityService(); + Pair databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys, () -> identitySvc); issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index fc7bd1081e..c9abfefbe1 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -641,9 +641,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // search predicate val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) - val joinCashToParty = cashStatesSchema.join("owner") - val queryOwnerKey = BOB_PUBKEY.toBase58String() - criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryOwnerKey)) + val queryOwner = BOB.name.toString() + criteriaQuery.where(criteriaBuilder.equal(cashStatesSchema.get("owner"), queryOwner)) val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get("stateRef"), cashStatesSchema.get("stateRef")) criteriaQuery.where(joinVaultStatesToCash) @@ -726,9 +725,9 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // search predicate val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) - val joinCashToParty = cashStatesSchema.join("participants") - val queryParticipantKeys = firstCashState.state.data.participants.map { it.owningKey.toBase58String() } - criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryParticipantKeys)) + val queryParticipants = firstCashState.state.data.participants.map { it.nameOrNull().toString() } + val joinCashStateToParty = cashStatesSchema.joinSet("participants") + criteriaQuery.where(criteriaBuilder.and(joinCashStateToParty.`in`(queryParticipants))) val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get("stateRef"), cashStatesSchema.get("stateRef")) criteriaQuery.where(joinVaultStatesToCash) diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index 958f25d540..fc995fc2ee 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -174,7 +174,6 @@ class RequeryConfigurationTest : TestDependencyInjectionBase() { contractStateClassName = DummyContract.SingleOwnerState::class.java.name contractState = DummyContract.SingleOwnerState(owner = AnonymousParty(MEGA_CORP_PUBKEY)).serialize().bytes notaryName = txn.tx.notary!!.name.toString() - notaryKey = txn.tx.notary!!.owningKey.toBase58String() recordedTime = Instant.now() } return state 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 d2e7dc3980..62e1591b15 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 @@ -69,11 +69,11 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { lockId: UUID = UUID.randomUUID(), withIssuerRefs: Set? = null): List> { - val notaryName = if (notary != null) listOf(notary.name) else null - var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = notaryName) + val notaries = if (notary != null) listOf(notary) else null + var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = notaries) if (onlyFromIssuerParties != null || withIssuerRefs != null) { baseCriteria = baseCriteria.and(QueryCriteria.FungibleAssetQueryCriteria( - issuerPartyName = onlyFromIssuerParties?.toList(), + issuer = onlyFromIssuerParties?.toList(), issuerRef = withIssuerRefs?.toList())) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index b0c5e25802..53d3d00e55 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -11,6 +11,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.toBase58String import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* @@ -50,11 +51,20 @@ class VaultQueryTests : TestDependencyInjectionBase() { lateinit var notaryServices: MockServices val vaultSvc: VaultService get() = services.vaultService val vaultQuerySvc: VaultQueryService get() = services.vaultQueryService + val identitySvc: IdentityService = makeTestIdentityService() lateinit var database: CordaPersistence + // test cash notary + val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) } + val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) + val CASH_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CASH_NOTARY.nameOrNull()!!, CASH_NOTARY_KEY.public) + @Before fun setUp() { - val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY)) + // register additional identities + identitySvc.verifyAndRegisterIdentity(CASH_NOTARY_IDENTITY) + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) + val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), identitySvc = { identitySvc }) database = databaseAndServices.first services = databaseAndServices.second notaryServices = MockServices(DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) @@ -71,7 +81,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Ignore @Test fun createPersistentTestDb() { - val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = { identitySvc }) setUpDb(database, 5000) @@ -396,9 +406,6 @@ class VaultQueryTests : TestDependencyInjectionBase() { } } - val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) } - val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) - @Test fun `unconsumed states by notary`() { database.transaction { @@ -408,7 +415,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestDeals(listOf("123", "456", "789")) // DOCSTART VaultQueryExample4 - val criteria = VaultQueryCriteria(notaryName = listOf(CASH_NOTARY.name)) + val criteria = VaultQueryCriteria(notary = listOf(CASH_NOTARY)) val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample4 assertThat(results.states).hasSize(3) @@ -418,6 +425,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear states for single participant`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BIG_CORP_IDENTITY) services.fillWithSomeTestLinearStates(2, "TEST", participants = listOf(MEGA_CORP, MINI_CORP)) services.fillWithSomeTestDeals(listOf("456"), participants = listOf(MEGA_CORP, BIG_CORP)) @@ -433,13 +441,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear states for two participants`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BIG_CORP_IDENTITY) services.fillWithSomeTestLinearStates(2, "TEST", participants = listOf(MEGA_CORP, MINI_CORP)) services.fillWithSomeTestDeals(listOf("456"), participants = listOf(MEGA_CORP, BIG_CORP)) - services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP)) + services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(MEGA_CORP)) // DOCSTART VaultQueryExample5 - val criteria = LinearStateQueryCriteria(participants = listOf(MEGA_CORP, MINI_CORP)) + val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP, MINI_CORP)) val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample5 @@ -793,6 +802,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = DUMMY_CASH_ISSUER) services.fillWithSomeTestCash(200.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1)) @@ -1026,6 +1036,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 100, 100, Random(0L)) + @Suppress("OVERFLOW_EXPECTED") val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) vaultQuerySvc.queryBy(criteria, paging = pagingSpec) @@ -1375,7 +1386,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { database.transaction { - val parties = listOf(MEGA_CORP) + val parties = listOf(MINI_CORP) services.fillWithSomeTestLinearStates(2, "TEST") services.fillWithSomeTestDeals(listOf("456"), parties) @@ -1395,13 +1406,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets for specific issuer party and refs`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), ref = OpaqueBytes.of(1)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), ref = OpaqueBytes.of(2)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), ref = OpaqueBytes.of(3)) - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC), + val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC), issuerRef = listOf(BOC.ref(1).reference, BOC.ref(2).reference)) val results = vaultQuerySvc.queryBy>(criteria) assertThat(results.states).hasSize(2) @@ -1424,11 +1436,12 @@ class VaultQueryTests : TestDependencyInjectionBase() { val chfCashIssuerServices = MockServices(chfCashIssuerKey) database.transaction { + services.fillWithSomeTestCash(100.POUNDS, gbpCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (gbpCashIssuer)) services.fillWithSomeTestCash(100.DOLLARS, usdCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (usdCashIssuer)) services.fillWithSomeTestCash(100.SWISS_FRANCS, chfCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (chfCashIssuer)) - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(gbpCashIssuer.party, usdCashIssuer.party)) + val criteria = FungibleAssetQueryCriteria(issuer = listOf(gbpCashIssuer.party, usdCashIssuer.party)) val results = vaultQuerySvc.queryBy>(criteria) assertThat(results.states).hasSize(2) } @@ -1438,17 +1451,16 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun `unconsumed fungible assets by owner`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1)) + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = BOC.ref(1)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), - issuedBy = MEGA_CORP.ref(0), ownedBy = (MEGA_CORP)) + issuedBy = MEGA_CORP.ref(0), ownedBy = (MINI_CORP)) val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP)) val results = vaultQuerySvc.queryBy>(criteria) - assertThat(results.states).hasSize(1) + assertThat(results.states).hasSize(1) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) } } - @Test fun `unconsumed fungible states for owners`() { database.transaction { @@ -1464,7 +1476,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample5.2 - assertThat(results.states).hasSize(1) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) + assertThat(results.states).hasSize(2) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) } } @@ -1555,12 +1567,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets for issuer party`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1))) // DOCSTART VaultQueryExample14 - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC)) + val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC)) val results = vaultQuerySvc.queryBy>(criteria) // DOCEND VaultQueryExample14 @@ -1823,6 +1836,41 @@ class VaultQueryTests : TestDependencyInjectionBase() { } } + @Test + fun `unconsumed linear heads for single participant`() { + database.transaction { + identitySvc.verifyAndRegisterIdentity(ALICE_IDENTITY) + services.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE)) + services.fillWithSomeTestLinearStates(1) + services.fillWithSomeTestLinearStates(1, "TEST3") + + val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE)) + val results = vaultQuerySvc.queryBy(linearStateCriteria) + + assertThat(results.states).hasSize(1) + assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") + } + } + + @Test + fun `unconsumed linear heads for multiple participants`() { + database.transaction { + identitySvc.verifyAndRegisterIdentity(ALICE_IDENTITY) + identitySvc.verifyAndRegisterIdentity(BOB_IDENTITY) + identitySvc.verifyAndRegisterIdentity(CHARLIE_IDENTITY) + + services.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE,BOB,CHARLIE)) + services.fillWithSomeTestLinearStates(1) + services.fillWithSomeTestLinearStates(1, "TEST3") + + val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE,BOB,CHARLIE)) + val results = vaultQuerySvc.queryBy(linearStateCriteria) + + assertThat(results.states).hasSize(1) + assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") + } + } + @Test fun `unconsumed linear heads where external id is null`() { database.transaction { diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 663fe30dcf..4fb12b6c3d 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -5,6 +5,7 @@ package net.corda.testing import com.nhaarman.mockito_kotlin.spy import com.nhaarman.mockito_kotlin.whenever +import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.StateRef import net.corda.core.crypto.* import net.corda.core.identity.Party @@ -88,7 +89,9 @@ val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference val ALL_TEST_KEYS: List get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY) -val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTARY_IDENTITY) +val DUMMY_CASH_ISSUER_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_CASH_ISSUER.party as Party) + +val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_CASH_ISSUER_IDENTITY, DUMMY_NOTARY_IDENTITY) val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptySet(), DUMMY_CA.certificate.cert) val MOCK_HOST_AND_PORT = NetworkHostAndPort("mockHost", 30000) diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt index a3617160df..319dba7157 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestConstants.kt @@ -61,6 +61,7 @@ val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=Rome,C=IT"), BOB_K val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) } /** Dummy individual identity for tests and simulations */ +val CHARLIE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CHARLIE) val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=Athens,C=GR"), CHARLIE_KEY.public) val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt index 421de71ba6..ac66ff9560 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyDealContract.kt @@ -42,6 +42,7 @@ class DummyDealContract : Contract { override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is DummyDealStateSchemaV1 -> DummyDealStateSchemaV1.PersistentDummyDealState( + _participants = participants.toSet(), uid = linearId ) else -> throw IllegalArgumentException("Unrecognised schema $schema") diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt index 2ed7cc7a9d..d8273f77e4 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyLinearContract.kt @@ -50,6 +50,7 @@ class DummyLinearContract : Contract { override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is DummyLinearStateSchemaV1 -> DummyLinearStateSchemaV1.PersistentDummyLinearState( + participants = participants.toMutableSet(), externalId = linearId.externalId, uuid = linearId.id, linearString = linearString, @@ -58,6 +59,7 @@ class DummyLinearContract : Contract { linearBoolean = linearBoolean ) is DummyLinearStateSchemaV2 -> DummyLinearStateSchemaV2.PersistentDummyLinearState( + _participants = participants.toSet(), uid = linearId, linearString = linearString, linearNumber = linearNumber, diff --git a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt index b6cfb9daeb..6530f7b81a 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/contracts/VaultFiller.kt @@ -119,14 +119,14 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, issuedBy: PartyAndReference = DUMMY_CASH_ISSUER): Vault { val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng) - val myKey: PublicKey = ownedBy?.owningKey ?: myInfo.legalIdentity.owningKey - val me = AnonymousParty(myKey) + val myKey = ownedBy?.owningKey ?: myInfo.legalIdentity.owningKey + val anonParty = AnonymousParty(myKey) // We will allocate one state to one transaction, for simplicities sake. val cash = Cash() val transactions: List = amounts.map { pennies -> val issuance = TransactionBuilder(null as Party?) - cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), me, outputNotary) + cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), anonParty, outputNotary) return@map issuerServices.signInitialTransaction(issuance, issuedBy.party.owningKey) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index 0b4ff61add..c468025bda 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -217,13 +217,15 @@ fun makeTestDatabaseProperties(): Properties { fun makeTestIdentityService() = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) -fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), keys: List = listOf(MEGA_CORP_KEY)): Pair { +fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), + keys: List = listOf(MEGA_CORP_KEY), + identitySvc: ()-> IdentityService = { makeTestIdentityService() }): Pair { val dataSourceProps = makeTestDataSourceProperties() val databaseProperties = makeTestDatabaseProperties() - val database = configureDatabase(dataSourceProps, databaseProperties, identitySvc = ::makeTestIdentityService) + val database = configureDatabase(dataSourceProps, databaseProperties, identitySvc = identitySvc) val mockService = database.transaction { - val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties, identitySvc = ::makeTestIdentityService) + val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties, identitySvc = identitySvc) object : MockServices(*(keys.toTypedArray())) { override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt index 7bf754473c..33c6a44c8e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyDealStateSchemaV1.kt @@ -1,6 +1,7 @@ package net.corda.testing.schemas import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema import javax.persistence.Entity @@ -21,8 +22,11 @@ object DummyDealStateSchemaV1 : MappedSchema(schemaFamily = DummyDealStateSchema @Table(name = "dummy_deal_states") class PersistentDummyDealState( /** parent attributes */ + @Transient + val _participants: Set, + @Transient val uid: UniqueIdentifier - ) : CommonSchemaV1.LinearState(uid = uid) + ) : CommonSchemaV1.LinearState(uid, _participants) } diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt index b8e490b04f..e25018bd9c 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV1.kt @@ -1,13 +1,12 @@ package net.corda.testing.schemas +import net.corda.core.contracts.ContractState +import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import java.time.Instant import java.util.* -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Index -import javax.persistence.Table +import javax.persistence.* /** * An object used to fully qualify the [DummyLinearStateSchema] family name (i.e. independent of version). @@ -24,6 +23,12 @@ object DummyLinearStateSchemaV1 : MappedSchema(schemaFamily = DummyLinearStateSc indexes = arrayOf(Index(name = "external_id_idx", columnList = "external_id"), Index(name = "uuid_idx", columnList = "uuid"))) class PersistentDummyLinearState( + /** [ContractState] attributes */ + + /** X500Name of participant parties **/ + @ElementCollection + var participants: MutableSet, + /** * UniqueIdentifier */ diff --git a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt index 44b2df08e0..35ad6880c5 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/schemas/DummyLinearStateSchemaV2.kt @@ -1,6 +1,7 @@ package net.corda.testing.schemas import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema import javax.persistence.Column @@ -25,7 +26,10 @@ object DummyLinearStateSchemaV2 : MappedSchema(schemaFamily = DummyLinearStateSc @Column(name = "linear_boolean") var linearBoolean: Boolean, /** parent attributes */ + @Transient + val _participants: Set, + @Transient val uid: UniqueIdentifier - ) : CommonSchemaV1.LinearState(uid = uid) + ) : CommonSchemaV1.LinearState(uid, _participants) } From e453fcfdf162f5754dc9d3521bede87dcbbd21f8 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko <31008341+vkolomeyko@users.noreply.github.com> Date: Thu, 17 Aug 2017 10:41:38 +0100 Subject: [PATCH 019/101] Upgrade Kotlin version to 1.1.4 (#1267) --- constants.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/constants.properties b/constants.properties index b6155a12a4..420ba0853f 100644 --- a/constants.properties +++ b/constants.properties @@ -1,5 +1,5 @@ gradlePluginsVersion=0.15.0 -kotlinVersion=1.1.1 +kotlinVersion=1.1.4 guavaVersion=21.0 bouncycastleVersion=1.57 -typesafeConfigVersion=1.3.1 +typesafeConfigVersion=1.3.1 \ No newline at end of file From f38954e55057c6dfe2a22508351c13e639e68fd5 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 17 Aug 2017 11:04:30 +0100 Subject: [PATCH 020/101] Re-re-move Java test to the correct location, fix compile issue Somehow this slipped past Gradle that I hadn't changed the serializer factory here to take the needed argument now it's no longer defaulted --- .../amqp/JavaSerializationOutputTests.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) rename node-api/src/test/{kotlin => java}/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java (95%) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java similarity index 95% rename from node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java rename to node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index ff20bffe97..171b9cbe72 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -168,8 +168,9 @@ public class JavaSerializationOutputTests { } private Object serdes(Object obj) throws NotSerializableException { - SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); - SerializationOutput ser = new SerializationOutput(factory); + SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); + SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); + SerializationOutput ser = new SerializationOutput(factory1); SerializedBytes bytes = ser.serialize(obj); DecoderImpl decoder = new DecoderImpl(); @@ -187,13 +188,13 @@ public class JavaSerializationOutputTests { Envelope result = (Envelope) decoder.readObject(); assertTrue(result != null); - DeserializationInput des = new DeserializationInput(); + DeserializationInput des = new DeserializationInput(factory2); Object desObj = des.deserialize(bytes, Object.class); assertTrue(Objects.deepEquals(obj, desObj)); // Now repeat with a re-used factory - SerializationOutput ser2 = new SerializationOutput(factory); - DeserializationInput des2 = new DeserializationInput(factory); + SerializationOutput ser2 = new SerializationOutput(factory1); + DeserializationInput des2 = new DeserializationInput(factory1); Object desObj2 = des2.deserialize(ser2.serialize(obj), Object.class); assertTrue(Objects.deepEquals(obj, desObj2)); // TODO: check schema is as expected From 1672b4aa0a2cc55f158a66b1c783b3e1a5dbb400 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Thu, 17 Aug 2017 11:15:04 +0100 Subject: [PATCH 021/101] Simple deserialisation fixes. (#1243) * Fix typo: prefered -> preferred * Simplify KryoVerifierSerializationScheme and resolve warning. * Add a custom serialiser for PrivacySeed so that we can avoid invoking RNG. --- .../core/serialization/SerializationAPI.kt | 2 +- .../serialization/AMQPSerializationScheme.kt | 2 +- .../serialization/DefaultKryoCustomizer.kt | 21 +++++++++++++-- .../serialization/SerializationScheme.kt | 6 ++--- .../internal/serialization/KryoTests.kt | 26 +++++++++++++++++++ .../corda/testing/SerializationTestHelpers.kt | 4 +-- .../kotlin/net/corda/verifier/Verifier.kt | 12 +++------ 7 files changed, 55 insertions(+), 18 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index 7dab1e0243..b9f2094394 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -39,7 +39,7 @@ interface SerializationContext { /** * When serializing, use the format this header sequence represents. */ - val preferedSerializationVersion: ByteSequence + val preferredSerializationVersion: ByteSequence /** * The class loader to use for deserialization. */ diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index eee7ed73c6..bd5e883603 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -11,7 +11,7 @@ import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import java.util.concurrent.ConcurrentHashMap -internal val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferedSerializationVersion == AmqpHeaderV1_0 +internal val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0 abstract class AbstractAMQPSerializationScheme : SerializationScheme { internal companion object { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index f67d41841d..9cb3b8ea57 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -6,11 +6,11 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer -import com.esotericsoftware.kryo.util.MapReferenceResolver import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.guava.* +import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.composite.CompositeKey import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.CordaPluginRegistry @@ -110,6 +110,9 @@ object DefaultKryoCustomizer { register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer) register(PartyAndCertificate::class.java, PartyAndCertificateSerializer) + // Don't deserialize PrivacySalt via its default constructor. + register(PrivacySalt::class.java, PrivacySaltSerializer) + val customization = KryoSerializationCustomization(this) pluginRegistries.forEach { it.customizeSerialization(customization) } } @@ -153,4 +156,18 @@ object DefaultKryoCustomizer { return list.toNonEmptySet() } } -} \ No newline at end of file + + /* + * Avoid deserialising PrivacySalt via its default constructor + * because the random number generator may not be available. + */ + private object PrivacySaltSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: PrivacySalt) { + output.writeBytesWithLength(obj.bytes) + } + + override fun read(kryo: Kryo, input: Input, type: Class): PrivacySalt { + return PrivacySalt(input.readBytesWithLength()) + } + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index 4ab8c3c27e..a6e51868a1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -28,7 +28,7 @@ object NotSupportedSeralizationScheme : SerializationScheme { override fun serialize(obj: T, context: SerializationContext): SerializedBytes = doThrow() } -data class SerializationContextImpl(override val preferedSerializationVersion: ByteSequence, +data class SerializationContextImpl(override val preferredSerializationVersion: ByteSequence, override val deserializationClassLoader: ClassLoader, override val whitelist: ClassWhitelist, override val properties: Map, @@ -52,7 +52,7 @@ data class SerializationContextImpl(override val preferedSerializationVersion: B }) } - override fun withPreferredSerializationVersion(versionHeader: ByteSequence) = copy(preferedSerializationVersion = versionHeader) + override fun withPreferredSerializationVersion(versionHeader: ByteSequence) = copy(preferredSerializationVersion = versionHeader) } private const val HEADER_SIZE: Int = 8 @@ -81,7 +81,7 @@ open class SerializationFactoryImpl : SerializationFactory { override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T = schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context) override fun serialize(obj: T, context: SerializationContext): SerializedBytes { - return schemeFor(context.preferedSerializationVersion, context.useCase).serialize(obj, context) + return schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context) } fun registerScheme(scheme: SerializationScheme) { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index 66918ac812..bbe54aeb40 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -5,6 +5,7 @@ import com.esotericsoftware.kryo.KryoSerializable import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.google.common.primitives.Ints +import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.* import net.corda.core.serialization.* import net.corda.core.utilities.ProgressTracker @@ -16,7 +17,9 @@ import net.corda.testing.TestDependencyInjectionBase import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.InputStream @@ -28,6 +31,9 @@ class KryoTests : TestDependencyInjectionBase() { private lateinit var factory: SerializationFactory private lateinit var context: SerializationContext + @get:Rule + val expectedEx: ExpectedException = ExpectedException.none() + @Before fun setup() { factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } @@ -157,6 +163,26 @@ class KryoTests : TestDependencyInjectionBase() { override fun toString(): String = "Cyclic($value)" } + @Test + fun `serialize - deserialize PrivacySalt`() { + val expected = PrivacySalt(byteArrayOf( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32 + )) + val serializedBytes = expected.serialize(factory, context) + val actual = serializedBytes.deserialize(factory, context) + assertEquals(expected, actual) + } + + @Test + fun `all-zero PrivacySalt not allowed`() { + expectedEx.expect(IllegalArgumentException::class.java) + expectedEx.expectMessage("Privacy salt should not be all zeros.") + PrivacySalt(ByteArray(32)) + } + @CordaSerializable private object TestSingleton diff --git a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index 9ec3760622..399bdcf922 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -111,8 +111,8 @@ class TestSerializationContext : SerializationContext { override fun toString(): String = stackTrace?.joinToString("\n") ?: "null" - override val preferedSerializationVersion: ByteSequence - get() = delegate!!.preferedSerializationVersion + override val preferredSerializationVersion: ByteSequence + get() = delegate!!.preferredSerializationVersion override val deserializationClassLoader: ClassLoader get() = delegate!!.deserializationClassLoader override val whitelist: ClassWhitelist diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index fec528f883..92c7a2c179 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -1,6 +1,5 @@ package net.corda.verifier -import com.esotericsoftware.kryo.pool.KryoPool import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions @@ -98,15 +97,10 @@ class Verifier { class KryoVerifierSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { - return byteSequence.equals(KryoHeaderV0_1) && target == SerializationContext.UseCase.P2P + return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P } - override fun rpcClientKryoPool(context: SerializationContext): KryoPool { - throw UnsupportedOperationException() - } - - override fun rpcServerKryoPool(context: SerializationContext): KryoPool { - throw UnsupportedOperationException() - } + override fun rpcClientKryoPool(context: SerializationContext) = throw UnsupportedOperationException() + override fun rpcServerKryoPool(context: SerializationContext) = throw UnsupportedOperationException() } } \ No newline at end of file From 4286065872f9b793ea95458fb3222005bfe993d9 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 17 Aug 2017 10:51:49 +0100 Subject: [PATCH 022/101] Eliminate explicit Kryo configuration to allow flexibility for AMQP --- .../internal/serialization/SerializationTokenTest.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt index 03ab48214d..0ac1acdc18 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt @@ -7,7 +7,6 @@ import com.nhaarman.mockito_kotlin.mock import net.corda.core.node.ServiceHub import net.corda.core.serialization.* import net.corda.core.utilities.OpaqueBytes -import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.testing.TestDependencyInjectionBase import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -21,13 +20,8 @@ class SerializationTokenTest : TestDependencyInjectionBase() { @Before fun setup() { - factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } - context = SerializationContextImpl(KryoHeaderV0_1, - javaClass.classLoader, - AllWhitelist, - emptyMap(), - true, - SerializationContext.UseCase.P2P) + factory = SerializationDefaults.SERIALIZATION_FACTORY + context = SerializationDefaults.CHECKPOINT_CONTEXT.withWhitelisted(SingletonSerializationToken::class.java) } // Large tokenizable object so we can tell from the smaller number of serialized bytes it was actually tokenized From 4eb58b874a0b6a533d5951f7568092f6a6b55baa Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 17 Aug 2017 12:02:44 +0100 Subject: [PATCH 023/101] Additional fixes to the tutorial. --- docs/source/hello-world-contract.rst | 11 +- docs/source/hello-world-running.rst | 54 ++------ docs/source/hello-world-template.rst | 13 +- .../resources/simple-tutorial-transaction.png | Bin 176926 -> 212959 bytes docs/source/tut-two-party-contract.rst | 20 ++- docs/source/tut-two-party-flow.rst | 126 +++++++++--------- docs/source/tut-two-party-index.rst | 3 +- docs/source/tut-two-party-introduction.rst | 10 +- docs/source/tut-two-party-running.rst | 28 ---- 9 files changed, 112 insertions(+), 153 deletions(-) delete mode 100644 docs/source/tut-two-party-running.rst diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 253020441e..59b3b81da8 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -67,7 +67,7 @@ transfer them or redeem them for cash. One way to enforce this behaviour would b * Its value must be non-negative * The lender and the borrower cannot be the same entity - * The IOU's borrower must sign the transaction + * The IOU's lender must sign the transaction We can picture this transaction as follows: @@ -122,7 +122,6 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi package com.template.contract; - import com.google.common.collect.ImmutableSet; import com.template.state.IOUState; import net.corda.core.contracts.AuthenticatedObject; import net.corda.core.contracts.CommandData; @@ -155,7 +154,7 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi check.using("The lender and the borrower cannot be the same entity.", lender != borrower); // Constraints on the signers. - check.using("There must only be one signer.", ImmutableSet.of(command.getSigners()).size() == 1); + check.using("There must only be one signer.", command.getSigners().size() == 1); check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey())); return null; @@ -179,7 +178,7 @@ The first thing we add to our contract is a *command*. Commands serve two functi example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one redeeming an IOU * They allow us to define the required signers for the transaction. For example, IOU creation might require signatures - from the borrower alone, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender + from the lender only, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender Our contract has one command, a ``Create`` command. All commands must implement the ``CommandData`` interface. @@ -219,7 +218,7 @@ following are true: * The transaction has inputs * The transaction doesn't have exactly one output * The IOU itself is invalid -* The transaction doesn't require the borrower's signature +* The transaction doesn't require the lender's signature Command constraints ~~~~~~~~~~~~~~~~~~~ @@ -270,7 +269,7 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta * Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a ``Create`` command * The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower - must be different entities. + must be different entities Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 2696af6b23..1baaccf4e3 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -16,27 +16,25 @@ Kotlin) file. We won't be using it, and it will cause build errors unless we rem Deploying our CorDapp --------------------- -Let's take a look at the nodes we're going to deploy. Open the project's build file under ``java-source/build.gradle`` -or ``kotlin-source/build.gradle`` and scroll down to the ``task deployNodes`` section. This section defines four -nodes - the Controller, and NodeA, NodeB and NodeC: +Let's take a look at the nodes we're going to deploy. Open the project's ``build.gradle`` file and scroll down to the +``task deployNodes`` section. This section defines three nodes - the Controller, NodeA, and NodeB: .. container:: codeset .. code-block:: kotlin - task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" + networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK" node { - name "CN=Controller,O=R3,OU=corda,L=London,C=GB" + name "CN=Controller,O=R3,OU=corda,L=London,C=UK" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 - webPort 10004 cordapps = [] } node { - name "CN=NodeA,O=NodeA,L=London,C=GB" + name "CN=NodeA,O=NodeA,L=London,C=UK" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -53,15 +51,6 @@ nodes - the Controller, and NodeA, NodeB and NodeC: cordapps = [] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] } - node { - name "CN=NodeC,O=NodeC,L=Paris,C=FR" - advertisedServices = [] - p2pPort 10011 - rpcPort 10012 - webPort 10013 - cordapps = [] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } } We have three standard nodes, plus a special Controller node that is running the network map service, and is also @@ -85,8 +74,8 @@ We can do that now by running the following commands from the root of the projec Running the nodes ----------------- -Running ``deployNodes`` will build the nodes under both ``java-source/build/nodes`` and ``kotlin-source/build/nodes``. -If we navigate to one of these folders, we'll see four node folder. Each node folder has the following structure: +Running ``deployNodes`` will build the nodes under ``build/nodes``. If we navigate to one of these folders, we'll see +the three node folders. Each node folder has the following structure: .. code:: python @@ -102,17 +91,11 @@ Let's start the nodes by running the following commands from the root of the pro .. code:: python - // On Windows for a Java CorDapp - java-source/build/nodes/runnodes.bat + // On Windows + build/nodes/runnodes.bat - // On Windows for a Kotlin CorDapp - kotlin-source/build/nodes/runnodes.bat - - // On Mac for a Java CorDapp - java-source/build/nodes/runnodes - - // On Mac for a Kotlin CorDapp - kotlin-source/build/nodes/runnodes + // On Mac + build/nodes/runnodes This will start a terminal window for each node, and an additional terminal window for each node's webserver - eight terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays @@ -143,10 +126,8 @@ We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing: start IOUFlow iouValue: 99, otherParty: "NodeB" -Node A and Node B will automatically agree an IOU. - -If the flow worked, it should have led to the recording of a new IOU in the vaults of both Node A and Node B. Equally -importantly, Node C - although it sits on the same network - should not be aware of this transaction. +Node A and Node B will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU +in the vaults of both Node A and Node B. We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run`` will display a list of the available commands. We can examine the contents of a node's vault by running: @@ -183,13 +164,6 @@ The vaults of Node A and Node B should both display the following output: index: 0 second: "(observable)" -But the vault of Node C should output nothing! - -.. code:: python - - first: [] - second: "(observable)" - Conclusion ---------- We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Like all CorDapps, our diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 6ef1be4905..acb131dc6f 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -40,22 +40,25 @@ Open a terminal window in the directory where you want to download the CorDapp t Template structure ------------------ We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. To -implement our IOU CorDapp in Java, we'll only need to modify three files: +implement our IOU CorDapp in Java, we'll need to modify three files. For Kotlin, we'll simply be modifying the +``App.kt`` file: .. container:: codeset .. code-block:: java // 1. The state - java-source/src/main/java/com/template/state/TemplateState.java + src/main/java/com/template/state/TemplateState.java // 2. The contract - java-source/src/main/java/com/template/contract/TemplateContract.java + src/main/java/com/template/contract/TemplateContract.java // 3. The flow - java-source/src/main/java/com/template/flow/TemplateFlow.java + src/main/java/com/template/flow/TemplateFlow.java -For Kotlin, we'll simply be modifying the ``App.kt`` file. + .. code-block:: kotlin + + src/main/kotlin/com/template/App.kt Progress so far --------------- diff --git a/docs/source/resources/simple-tutorial-transaction.png b/docs/source/resources/simple-tutorial-transaction.png index f881346b06a881de39419087c270dc89900a3a0b..7b1b128ddaf7c45416e3cd3b8f74c64a7589d0a2 100644 GIT binary patch literal 212959 zcmeFac|4T;_dkx1rLt2}WGy=-DqF}BrNwS+$vSprXG~eMmZ&77Rb@-EWY1b8WDVK( zZS3oq`MzeThPyuZ`|bVv=l6JYKORN1T-SM>^E}V9G-AlC!^YjYv7|vFgr?9Z;On-M7YSy0?8fb6?{ zv3?hdt(a&R+m8Ll6@ypFAUAHH2$-Gwy822D2+l=hhbCsC*U+%tx^`ZqI|&Yx4M^CL z^6VbCY?4-11D5$1glOQ7CNh{FIIE?@L1(q)cM}7V||>l zHZD*6EV|6N$tLr6o#yqNz`Q?zIra6@8RyzMg{V86b9)q(j$Er9y`;5#_C6~UwM3=l z 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

2b*q+ay>?318{h-Omx{4H(Be#M62IH-!&Dt5 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 literal 176926 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�VUiPO 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 requiredSigners = ImmutableList.of(me.getOwningKey(), otherParty.getOwningKey()); @@ -56,65 +66,42 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: txBuilder.verify(getServiceHub()); // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().toSignedTransaction(txBuilder); + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Obtaining the counterparty's signature - final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, null)); + final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, CollectSignaturesFlow.Companion.tracker())); // Finalising the transaction. subFlow(new FinalityFlow(fullySignedTx)); -To make the lender a required signer, we simply add the lender's public key to the list of signers on the command. + return null; + +To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command. ``CollectSignaturesFlow``, meanwhile, takes a transaction signed by the flow initiator, and returns a transaction signed by all the transaction's other required signers. We then pass this fully-signed transaction into ``FinalityFlow``. -The lender's flow ------------------ -Reorganising our class -^^^^^^^^^^^^^^^^^^^^^^ -Before we define the lender's flow, let's reorganise ``IOUFlow.java``/``IOUFlow.kt`` a little bit: - -* Rename ``IOUFlow`` to ``Initiator`` -* In Java, make the ``Initiator`` class static, rename its constructor to match the new name, and move the definition - inside an enclosing ``IOUFlow`` class -* In Kotlin, move the definition of ``Initiator`` class inside an enclosing ``IOUFlow`` singleton object - -We will end up with the following structure: - -.. container:: codeset - - .. code-block:: kotlin - - object IOUFlow { - @InitiatingFlow - @StartableByRPC - class Initiator(val iouValue: Int, - val otherParty: Party) : FlowLogic() { - - .. code-block:: java - - public class IOUFlow { - @InitiatingFlow - @StartableByRPC - public static class Initiator extends FlowLogic { - -Writing the lender's flow -^^^^^^^^^^^^^^^^^^^^^^^^^ +Creating the borrower's flow +---------------------------- We're now ready to write the lender's flow, which will respond to the borrower's attempt to gather our signature. - -Inside the ``IOUFlow`` class/singleton object, add the following class: +In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file in Kotlin, add the following class: .. container:: codeset .. code-block:: kotlin - @InitiatedBy(Initiator::class) - class Acceptor(val otherParty: Party) : FlowLogic() { + ... + + import net.corda.core.transactions.SignedTransaction + + ... + + @InitiatedBy(IOUFlow::class) + class IOUFlowResponder(val otherParty: Party) : FlowLogic() { @Suspendable override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherParty) { + val signTransactionFlow = object : SignTransactionFlow(otherParty, SignTransactionFlow.tracker()) { override fun checkTransaction(stx: SignedTransaction) = requireThat { val output = stx.tx.outputs.single().data "This must be an IOU transaction." using (output is IOUState) @@ -129,12 +116,26 @@ Inside the ``IOUFlow`` class/singleton object, add the following class: .. code-block:: java - @InitiatedBy(Initiator.class) - public static class Acceptor extends FlowLogic { + package com.template.flow; + import co.paralleluniverse.fibers.Suspendable; + import com.template.state.IOUState; + import net.corda.core.contracts.ContractState; + import net.corda.core.flows.FlowException; + import net.corda.core.flows.FlowLogic; + import net.corda.core.flows.InitiatedBy; + import net.corda.core.flows.SignTransactionFlow; + import net.corda.core.identity.Party; + import net.corda.core.transactions.SignedTransaction; + import net.corda.core.utilities.ProgressTracker; + + import static net.corda.core.contracts.ContractsDSL.requireThat; + + @InitiatedBy(IOUFlow.class) + public class IOUFlowResponder extends FlowLogic { private final Party otherParty; - public Acceptor(Party otherParty) { + public IOUFlowResponder(Party otherParty) { this.otherParty = otherParty; } @@ -142,8 +143,8 @@ Inside the ``IOUFlow`` class/singleton object, add the following class: @Override public Void call() throws FlowException { class signTxFlow extends SignTransactionFlow { - private signTxFlow(Party otherParty) { - super(otherParty, null); + private signTxFlow(Party otherParty, ProgressTracker progressTracker) { + super(otherParty, progressTracker); } @Override @@ -158,18 +159,19 @@ Inside the ``IOUFlow`` class/singleton object, add the following class: } } - subFlow(new signTxFlow(otherParty)); + subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker())); return null; } } -As with the ``Initiator``, our ``Acceptor`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``. +As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden +``FlowLogic.call``. -The flow is annotated with ``InitiatedBy(Initiator.class)``, which means that your node will invoke ``Acceptor.call`` -when it receives a message from a instance of ``Initiator`` running on another node. What will this message from the -``Initiator`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that we'll be sent a -``SignedTransaction``, and are expected to send back our signature over that transaction. +The flow is annotated with ``InitiatedBy(IOUFlow.class)``, which means that your node will invoke +``IOUFlowResponder.call`` when it receives a message from a instance of ``Initiator`` running on another node. What +will this message from the ``IOUFlow`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that +we'll be sent a ``SignedTransaction``, and are expected to send back our signature over that transaction. We could handle this manually. However, there is also a pre-defined flow called ``SignTransactionFlow`` that can handle this process for us automatically. ``SignTransactionFlow`` is an abstract class, and we must subclass it and override @@ -179,7 +181,7 @@ Once we've defined the subclass, we invoke it using ``FlowLogic.subFlow``, and t and the lender's flow is conducted automatically. CheckTransactions -~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^ ``SignTransactionFlow`` will automatically verify the transaction and its signatures before signing it. However, just because a transaction is valid doesn't mean we necessarily want to sign. What if we don't want to deal with the counterparty in question, or the value is too high, or we're not happy with the transaction's structure? @@ -200,4 +202,4 @@ We can now run our updated CorDapp, using the instructions :doc:`here `_ -with an API and web front-end, and a set of example CorDapps in -`the main Corda repo `_, under ``samples``. An explanation of how to run these -samples :doc:`here `. - -As you write CorDapps, you can learn more about the API available :doc:`here `. - -If you get stuck at any point, please reach out on `Slack `_, -`Discourse `_, or `Stack Overflow `_. \ No newline at end of file From 204b2fc5563b7f47311de58837fcd4130fa62279 Mon Sep 17 00:00:00 2001 From: mkit Date: Thu, 17 Aug 2017 12:59:24 +0100 Subject: [PATCH 024/101] Use String instead of Class for the flowClass (#1270) --- core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt | 2 +- .../src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt b/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt index 5730a968a1..0698b40c42 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt @@ -7,7 +7,7 @@ import java.time.Instant */ data class FlowStackSnapshot( val time: Instant, - val flowClass: Class>, + val flowClass: String, val stackFrames: List ) { data class Frame( diff --git a/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt b/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt index d7c6728687..9ee727f418 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/FlowStackSnapshot.kt @@ -73,7 +73,7 @@ class FlowStackSnapshotFactoryImpl : FlowStackSnapshotFactory { } Frame(element, stackObjects) } - return FlowStackSnapshot(Instant.now(), flowClass, frames) + return FlowStackSnapshot(Instant.now(), flowClass.name, frames) } private val StackTraceElement.instrumentedAnnotation: Instrumented? get() { From de05a11511b8f5cd12bd8065b2782b5e52f3afa8 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Wed, 9 Aug 2017 21:04:59 +0100 Subject: [PATCH 025/101] Move toward allowing serialisation of concrete types --- .../serialization/amqp/MapSerializer.kt | 15 ++- .../serialization/amqp/DeserializeMapTests.kt | 93 +++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index c399243445..2853ba47ee 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -5,6 +5,7 @@ import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* +import kotlin.collections.LinkedHashMap import kotlin.collections.Map import kotlin.collections.iterator import kotlin.collections.map @@ -17,10 +18,16 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" companion object { - private val supportedTypes: Map>, (Map<*, *>) -> Map<*, *>> = mapOf( - Map::class.java to { map -> Collections.unmodifiableMap(map) }, - SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) }, - NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) } + private val supportedTypes: Map, (Map<*, *>) -> Map<*, *>> = mapOf( + Map::class.java to { map -> map }, + SortedMap::class.java to { map -> TreeMap(map) }, + NavigableMap::class.java to { map -> TreeMap(map) }, + Dictionary::class.java to { map -> Hashtable(map) }, + // concrete types for user convienience + HashMap::class.java to { map -> LinkedHashMap(map) }, + LinkedHashMap::class.java to { map -> LinkedHashMap(map) }, + TreeMap::class.java to { map -> TreeMap(map) }, + Hashtable::class.java to { map -> Hashtable(map) } ) private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> { diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt new file mode 100644 index 0000000000..da16929ccb --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -0,0 +1,93 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.junit.Test +import java.util.* +import org.apache.qpid.proton.codec.Data + +class DeserializeCollectionTests { +class TestSerializationOutput( + private val verbose: Boolean, + serializerFactory: SerializerFactory = SerializerFactory()) : SerializationOutput(serializerFactory) { + + override fun writeSchema(schema: Schema, data: Data) { + if (verbose) println(schema) + super.writeSchema(schema, data) + } + } + companion object { + /** + * If you want to see the schema encoded into the envelope after serialisation change this to true + */ + private const val VERBOSE = false + } + + val sf = SerializerFactory() + + @Test + fun mapTest() { + data class C(val c: Map) + val c = C (mapOf("A" to 1, "B" to 2)) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun sortedMapTest() { + data class C(val c: SortedMap) + val c = C(sortedMapOf ("A" to 1, "B" to 2)) + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun dictionaryTest() { + data class C(val c: Dictionary) + var v : Hashtable = Hashtable() + v.put ("a", 1) + v.put ("b", 2) + val c = C(v) + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun navigableMapTest() { + data class C(val c: NavigableMap) + val c = C(TreeMap (mapOf("A" to 1, "B" to 3)).descendingMap()) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test(expected=java.lang.IllegalArgumentException::class) + fun HashMapTest() { + data class C(val c : HashMap) + val c = C (HashMap (mapOf("A" to 1, "B" to 2))) + + // expect this to throw + TestSerializationOutput(VERBOSE, sf).serialize(c) + } + + @Test + fun concreteTreeMapTest() { + data class C(val c: TreeMap) + val c = C(TreeMap (mapOf("A" to 1, "B" to 3))) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun concreteLinkedHashMapTest() { + data class C(val c : LinkedHashMap) + val c = C (LinkedHashMap (mapOf("A" to 1, "B" to 2))) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + val deserializedObj = DeserializationInput(sf).deserialize(serialisedBytes) + } + + + + +} From d2933ca8a3a6511a18d934ced64282b01edc52c0 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 10 Aug 2017 11:59:10 +0100 Subject: [PATCH 026/101] Finalise which map containers to serialize --- .../serialization/amqp/MapSerializer.kt | 15 +++-- .../serialization/amqp/SerializationHelper.kt | 2 + .../serialization/amqp/DeserializeMapTests.kt | 55 +++++++++++++------ .../amqp/SerializationOutputTests.kt | 2 +- 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index 2853ba47ee..eb45430453 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -18,18 +18,23 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" companion object { - private val supportedTypes: Map, (Map<*, *>) -> Map<*, *>> = mapOf( + private val supportedTypes: Map, (Map<*, *>) -> Map<*, *>> = mapOf( + // Interfaces Map::class.java to { map -> map }, SortedMap::class.java to { map -> TreeMap(map) }, NavigableMap::class.java to { map -> TreeMap(map) }, + // Sub types Dictionary::class.java to { map -> Hashtable(map) }, - // concrete types for user convienience - HashMap::class.java to { map -> LinkedHashMap(map) }, + AbstractMap::class.java to { map -> LinkedHashMap(map) }, + // concrete types LinkedHashMap::class.java to { map -> LinkedHashMap(map) }, TreeMap::class.java to { map -> TreeMap(map) }, - Hashtable::class.java to { map -> Hashtable(map) } + Hashtable::class.java to { map -> Hashtable(map) }, + WeakHashMap::class.java to { map -> WeakHashMap(map) } + // Explicitly disallowed + // - HashMap::class.java + // - EnumMap::class.java ) - private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> { return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index a8c15461ca..57efe40cab 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -35,6 +35,7 @@ internal fun constructorForDeserialization(type: Type): KFunction? { val kotlinConstructors = clazz.kotlin.constructors val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } for (kotlinConstructor in kotlinConstructors) { + println (kotlinConstructor) if (preferredCandidate == null && kotlinConstructors.size == 1 && !hasDefault) { preferredCandidate = kotlinConstructor } else if (preferredCandidate == null && kotlinConstructors.size == 2 && hasDefault && kotlinConstructor.parameters.isNotEmpty()) { @@ -44,6 +45,7 @@ internal fun constructorForDeserialization(type: Type): KFunction? { throw NotSerializableException("More than one constructor for $clazz is annotated with @CordaConstructor.") } preferredCandidate = kotlinConstructor + println (" -> $preferredCandidate") } } return preferredCandidate ?: throw NotSerializableException("No constructor for deserialization found for $clazz.") diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt index da16929ccb..3c9a615baa 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -5,7 +5,8 @@ import java.util.* import org.apache.qpid.proton.codec.Data class DeserializeCollectionTests { -class TestSerializationOutput( + + class TestSerializationOutput( private val verbose: Boolean, serializerFactory: SerializerFactory = SerializerFactory()) : SerializationOutput(serializerFactory) { @@ -32,6 +33,23 @@ class TestSerializationOutput( DeserializationInput(sf).deserialize(serialisedBytes) } + @Test + fun abstractMapFromMapOf() { + data class C(val c: AbstractMap) + val c = C (mapOf("A" to 1, "B" to 2) as AbstractMap) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun abstractMapFromTreeMap() { + data class C(val c: AbstractMap) + val c = C (TreeMap(mapOf("A" to 1, "B" to 2))) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } @Test fun sortedMapTest() { data class C(val c: SortedMap) @@ -40,6 +58,15 @@ class TestSerializationOutput( DeserializationInput(sf).deserialize(serialisedBytes) } + @Test + fun navigableMapTest() { + data class C(val c: NavigableMap) + val c = C(TreeMap (mapOf("A" to 1, "B" to 2)).descendingMap()) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + @Test fun dictionaryTest() { data class C(val c: Dictionary) @@ -51,17 +78,8 @@ class TestSerializationOutput( DeserializationInput(sf).deserialize(serialisedBytes) } - @Test - fun navigableMapTest() { - data class C(val c: NavigableMap) - val c = C(TreeMap (mapOf("A" to 1, "B" to 3)).descendingMap()) - - val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) - DeserializationInput(sf).deserialize(serialisedBytes) - } - @Test(expected=java.lang.IllegalArgumentException::class) - fun HashMapTest() { + fun hashMapTest() { data class C(val c : HashMap) val c = C (HashMap (mapOf("A" to 1, "B" to 2))) @@ -69,6 +87,15 @@ class TestSerializationOutput( TestSerializationOutput(VERBOSE, sf).serialize(c) } + @Test + fun weakHashMapTest() { + data class C(val c : WeakHashMap) + val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2))) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + @Test fun concreteTreeMapTest() { data class C(val c: TreeMap) @@ -84,10 +111,6 @@ class TestSerializationOutput( val c = C (LinkedHashMap (mapOf("A" to 1, "B" to 2))) val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) - val deserializedObj = DeserializationInput(sf).deserialize(serialisedBytes) + DeserializationInput(sf).deserialize(serialisedBytes) } - - - - } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 70a68e8cce..013fbaddc8 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -356,7 +356,7 @@ class SerializationOutputTests { serdes(obj) } - @Test(expected = NotSerializableException::class) + @Test fun `test TreeMap property`() { val obj = TreeMapWrapper(TreeMap()) obj.tree[456] = Foo("Fred", 123) From 8bebd6ea0bd861dcda4fab47b19fdf10e8508d98 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 11 Aug 2017 14:19:21 +0100 Subject: [PATCH 027/101] Rework To better deal with types we won't serialise * Add depricated jvm types to the blacklist * Add concrete types for user convenience * Add better "We won't serialise this because" error handling --- .../serialization/AllButBlacklisted.kt | 3 ++ .../serialization/amqp/MapSerializer.kt | 33 ++++++++++--------- .../serialization/amqp/SerializerFactory.kt | 2 +- .../serialization/amqp/DeserializeMapTests.kt | 29 +++++++++++----- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt index 81d72f0989..03cf25a68f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt @@ -1,6 +1,7 @@ package net.corda.nodeapi.internal.serialization import net.corda.core.serialization.ClassWhitelist +import sun.jvm.hotspot.memory.Dictionary import sun.misc.Unsafe import sun.security.util.Password import java.io.* @@ -39,6 +40,8 @@ object AllButBlacklisted : ClassWhitelist { Thread::class.java.name, HashSet::class.java.name, HashMap::class.java.name, + Dictionary::class.java.name, // Deprecated (marked obsolete) in the jdk + Hashtable::class.java.name, // see [Dictionary] ClassLoader::class.java.name, Handler::class.java.name, // MemoryHandler, StreamHandler Runtime::class.java.name, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index eb45430453..8b74459fe5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -20,20 +20,12 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact companion object { private val supportedTypes: Map, (Map<*, *>) -> Map<*, *>> = mapOf( // Interfaces - Map::class.java to { map -> map }, - SortedMap::class.java to { map -> TreeMap(map) }, - NavigableMap::class.java to { map -> TreeMap(map) }, - // Sub types - Dictionary::class.java to { map -> Hashtable(map) }, - AbstractMap::class.java to { map -> LinkedHashMap(map) }, - // concrete types + Map::class.java to { map -> Collections.unmodifiableMap(map) }, + SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) }, + NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) }, + // concrete classes for user convenience LinkedHashMap::class.java to { map -> LinkedHashMap(map) }, - TreeMap::class.java to { map -> TreeMap(map) }, - Hashtable::class.java to { map -> Hashtable(map) }, - WeakHashMap::class.java to { map -> WeakHashMap(map) } - // Explicitly disallowed - // - HashMap::class.java - // - EnumMap::class.java + TreeMap::class.java to { map -> TreeMap(map) } ) private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> { return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") @@ -52,7 +44,7 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact } override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { - obj.javaClass.checkNotUnorderedHashMap() + obj.javaClass.checkNotUnsupportedHashMap() // Write described data.withDescribed(typeNotation.descriptor) { // Write map @@ -77,8 +69,17 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1]) } -internal fun Class<*>.checkNotUnorderedHashMap() { +internal fun Class<*>.checkNotUnsupportedHashMap() { if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) { - throw IllegalArgumentException("Map type $this is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.") + throw IllegalArgumentException( + "Map type $this is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.") + } + else if (WeakHashMap::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException ("Weak references with map types not supported. Suggested fix: " + + "use java.util.LinkedHashMap instead.") + } + else if (Dictionary::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException ( + "Unable to serialise deprecated type $this. Suggested fix: prefer java.util.map implementations") } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 363909d0b3..64f9ba70c7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -272,7 +272,7 @@ class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer { val rawType = declaredType.rawType as Class<*> - rawType.checkNotUnorderedHashMap() + rawType.checkNotUnsupportedHashMap() return MapSerializer(declaredType, this) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt index 3c9a615baa..34c2a2f7c2 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -33,7 +33,7 @@ class DeserializeCollectionTests { DeserializationInput(sf).deserialize(serialisedBytes) } - @Test + @Test(expected=java.io.NotSerializableException::class) fun abstractMapFromMapOf() { data class C(val c: AbstractMap) val c = C (mapOf("A" to 1, "B" to 2) as AbstractMap) @@ -42,7 +42,7 @@ class DeserializeCollectionTests { DeserializationInput(sf).deserialize(serialisedBytes) } - @Test + @Test(expected=java.io.NotSerializableException::class) fun abstractMapFromTreeMap() { data class C(val c: AbstractMap) val c = C (TreeMap(mapOf("A" to 1, "B" to 2))) @@ -50,6 +50,7 @@ class DeserializeCollectionTests { val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) DeserializationInput(sf).deserialize(serialisedBytes) } + @Test fun sortedMapTest() { data class C(val c: SortedMap) @@ -67,15 +68,28 @@ class DeserializeCollectionTests { DeserializationInput(sf).deserialize(serialisedBytes) } - @Test + @Test(expected=java.lang.IllegalArgumentException::class) fun dictionaryTest() { data class C(val c: Dictionary) var v : Hashtable = Hashtable() v.put ("a", 1) v.put ("b", 2) val c = C(v) - val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) - DeserializationInput(sf).deserialize(serialisedBytes) + + // expected to throw + TestSerializationOutput(VERBOSE, sf).serialize(c) + } + + @Test(expected=java.lang.IllegalArgumentException::class) + fun hashtableTest() { + data class C(val c: Hashtable) + var v : Hashtable = Hashtable() + v.put ("a", 1) + v.put ("b", 2) + val c = C(v) + + // expected to throw + TestSerializationOutput(VERBOSE, sf).serialize(c) } @Test(expected=java.lang.IllegalArgumentException::class) @@ -87,13 +101,12 @@ class DeserializeCollectionTests { TestSerializationOutput(VERBOSE, sf).serialize(c) } - @Test + @Test(expected=java.lang.IllegalArgumentException::class) fun weakHashMapTest() { data class C(val c : WeakHashMap) val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2))) - val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) - DeserializationInput(sf).deserialize(serialisedBytes) + TestSerializationOutput(VERBOSE, sf).serialize(c) } @Test From c72ac2a102ad4d90a5b4130a27be37f952bb1a27 Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Fri, 11 Aug 2017 17:04:59 +0100 Subject: [PATCH 028/101] Add WeakHashMap to the blacklist --- .../corda/nodeapi/internal/serialization/AllButBlacklisted.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt index 03cf25a68f..56c9d5d317 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt @@ -40,6 +40,7 @@ object AllButBlacklisted : ClassWhitelist { Thread::class.java.name, HashSet::class.java.name, HashMap::class.java.name, + WeakHashMap::class.java.name, Dictionary::class.java.name, // Deprecated (marked obsolete) in the jdk Hashtable::class.java.name, // see [Dictionary] ClassLoader::class.java.name, From 6076d39ee4d21a57528ec8ad63e45bf2ebc2185d Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Tue, 15 Aug 2017 14:18:08 +0100 Subject: [PATCH 029/101] Remove spurious printlns --- .../nodeapi/internal/serialization/amqp/SerializationHelper.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index 57efe40cab..a8c15461ca 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -35,7 +35,6 @@ internal fun constructorForDeserialization(type: Type): KFunction? { val kotlinConstructors = clazz.kotlin.constructors val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } for (kotlinConstructor in kotlinConstructors) { - println (kotlinConstructor) if (preferredCandidate == null && kotlinConstructors.size == 1 && !hasDefault) { preferredCandidate = kotlinConstructor } else if (preferredCandidate == null && kotlinConstructors.size == 2 && hasDefault && kotlinConstructor.parameters.isNotEmpty()) { @@ -45,7 +44,6 @@ internal fun constructorForDeserialization(type: Type): KFunction? { throw NotSerializableException("More than one constructor for $clazz is annotated with @CordaConstructor.") } preferredCandidate = kotlinConstructor - println (" -> $preferredCandidate") } } return preferredCandidate ?: throw NotSerializableException("No constructor for deserialization found for $clazz.") From e6e6644fc95cd5452daeb3d18f113eda6777142c Mon Sep 17 00:00:00 2001 From: Katelyn Baker Date: Thu, 17 Aug 2017 14:17:59 +0100 Subject: [PATCH 030/101] Fixes to bring into line with changes API Can't have default factories now, they need building with whitelist and classloader. Also remove completely spurious import that broke everything, have a feeling that was something IntelliJ pulled in "to be helpful" --- .../serialization/AllButBlacklisted.kt | 1 - .../serialization/amqp/DeserializeMapTests.kt | 19 ++++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt index 56c9d5d317..eadd539414 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt @@ -1,7 +1,6 @@ package net.corda.nodeapi.internal.serialization import net.corda.core.serialization.ClassWhitelist -import sun.jvm.hotspot.memory.Dictionary import sun.misc.Unsafe import sun.security.util.Password import java.io.* diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt index 34c2a2f7c2..162756b06c 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -2,19 +2,8 @@ package net.corda.nodeapi.internal.serialization.amqp import org.junit.Test import java.util.* -import org.apache.qpid.proton.codec.Data class DeserializeCollectionTests { - - class TestSerializationOutput( - private val verbose: Boolean, - serializerFactory: SerializerFactory = SerializerFactory()) : SerializationOutput(serializerFactory) { - - override fun writeSchema(schema: Schema, data: Data) { - if (verbose) println(schema) - super.writeSchema(schema, data) - } - } companion object { /** * If you want to see the schema encoded into the envelope after serialisation change this to true @@ -22,7 +11,7 @@ class DeserializeCollectionTests { private const val VERBOSE = false } - val sf = SerializerFactory() + val sf = testDefaultFactory() @Test fun mapTest() { @@ -68,10 +57,10 @@ class DeserializeCollectionTests { DeserializationInput(sf).deserialize(serialisedBytes) } - @Test(expected=java.lang.IllegalArgumentException::class) + @Test(expected=java.io.NotSerializableException::class) fun dictionaryTest() { data class C(val c: Dictionary) - var v : Hashtable = Hashtable() + val v : Hashtable = Hashtable() v.put ("a", 1) v.put ("b", 2) val c = C(v) @@ -83,7 +72,7 @@ class DeserializeCollectionTests { @Test(expected=java.lang.IllegalArgumentException::class) fun hashtableTest() { data class C(val c: Hashtable) - var v : Hashtable = Hashtable() + val v : Hashtable = Hashtable() v.put ("a", 1) v.put ("b", 2) val c = C(v) From 734b48bad1f335dc0452b28633e5620d00ff6c92 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 17 Aug 2017 17:26:10 +0100 Subject: [PATCH 031/101] Fixes indentation of `note` box. --- docs/source/clientrpc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 590e338493..14695b7943 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -60,7 +60,7 @@ Currently, users need special permissions to start flows via RPC. These permissi ] .. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without -logging in. This will be changed in a future release. + logging in. This will be changed in a future release. Observables ----------- From 4a3848cc2a069a7d7b2d314305d746a0994d9bb6 Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 17 Aug 2017 14:55:01 +0100 Subject: [PATCH 032/101] Delete KryoAMQPSerializer and its registration As discussed with @rick-r3 this class is no longer fit for purpose because it may break reference equality if part of the graph been serialized with Kryo and part with AMQP --- .../serialization/CordaClassResolver.kt | 8 ----- .../serialization/KryoAMQPSerializer.kt | 34 ------------------- 2 files changed, 42 deletions(-) delete mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 9c180148fd..9d908e04bc 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -8,7 +8,6 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver import com.esotericsoftware.kryo.util.Util import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 import java.io.PrintWriter import java.lang.reflect.Modifier.isAbstract import java.nio.charset.StandardCharsets @@ -64,13 +63,6 @@ class CordaClassResolver(val serializationFactory: SerializationFactory, val ser } override fun registerImplicit(type: Class<*>): Registration { - // If something is not annotated, or AMQP is disabled, we stay serializing with Kryo. This will typically be the - // case for flow checkpoints (ignoring all cases where AMQP is disabled) since our top level messaging data structures - // are annotated and once we enter AMQP serialisation we stay with it for the entire object subgraph. - if (checkForAnnotation(type) && AMQP_ENABLED) { - // Build AMQP serializer - return register(Registration(type, KryoAMQPSerializer(serializationFactory, serializationContext), NAME.toInt())) - } val objectInstance = try { type.kotlin.objectInstance diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt deleted file mode 100644 index 90ea983388..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.corda.nodeapi.internal.serialization - -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory -import net.corda.core.utilities.sequence -import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 -import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput - -/** - * This [Kryo] custom [Serializer] switches the object graph of anything annotated with `@CordaSerializable` - * to using the AMQP serialization wire format, and simply writes that out as bytes to the wire. - * - * There is no need to write out the length, since this can be peeked out of the first few bytes of the stream. - */ -class KryoAMQPSerializer(val serializationFactory: SerializationFactory, val serializationContext: SerializationContext) : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: Any) { - val bytes = serializationFactory.serialize(obj, serializationContext.withPreferredSerializationVersion(AmqpHeaderV1_0)).bytes - // No need to write out the size since it's encoded within the AMQP. - output.write(bytes) - } - - override fun read(kryo: Kryo, input: Input, type: Class): Any { - // Use our helper functions to peek the size of the serialized object out of the AMQP byte stream. - val peekedBytes = input.readBytes(DeserializationInput.BYTES_NEEDED_TO_PEEK) - val size = DeserializationInput.peekSize(peekedBytes) - val allBytes = peekedBytes.copyOf(size) - input.readBytes(allBytes, peekedBytes.size, size - peekedBytes.size) - return serializationFactory.deserialize(allBytes.sequence(), type, serializationContext) - } -} From 51ed943272542213dd18c5e090de935717928b9e Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Thu, 17 Aug 2017 15:14:26 +0100 Subject: [PATCH 033/101] Remove SerializationFactory parameter from the places where it is not needed Also minor improvements to the looping/if-branching code and documentation changes Functionally, this change is a NOP --- .../net/corda/client/rpc/CordaRPCClient.kt | 2 +- .../rpc/serialization/SerializationScheme.kt | 5 +- .../kotlin/net/corda/nodeapi/RPCStructures.kt | 4 +- .../serialization/CordaClassResolver.kt | 27 ++++----- .../serialization/SerializationScheme.kt | 14 ++--- .../serialization/CordaClassResolverTests.kt | 60 +++++++++---------- .../internal/serialization/KryoTests.kt | 4 +- .../serialization/SerializationTokenTest.kt | 9 ++- .../kotlin/net/corda/node/internal/Node.kt | 7 +-- .../node/serialization/SerializationScheme.kt | 5 +- .../corda/node/utilities/X509UtilitiesTest.kt | 4 +- .../corda/testing/SerializationTestHelpers.kt | 4 +- .../kotlin/net/corda/verifier/Verifier.kt | 5 +- 13 files changed, 68 insertions(+), 82 deletions(-) diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 3ec8268c3e..a0a4a7ea1f 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -71,7 +71,7 @@ class CordaRPCClient( fun initialiseSerialization() { try { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { - registerScheme(KryoClientSerializationScheme(this)) + registerScheme(KryoClientSerializationScheme()) registerScheme(AMQPClientSerializationScheme()) } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt index 0bb26b93fb..817d741c64 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt @@ -3,21 +3,20 @@ package net.corda.client.rpc.serialization import com.esotericsoftware.kryo.pool.KryoPool import net.corda.client.rpc.internal.RpcClientObservableSerializer import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.RPCKryo import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1 -class KryoClientSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) { +class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P) } override fun rpcClientKryoPool(context: SerializationContext): KryoPool { return KryoPool.Builder { - DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, serializationFactory, context)).apply { classLoader = context.deserializationClassLoader } + DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader } }.build() } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt index a4be40c829..d99f17dab7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -6,10 +6,8 @@ import com.esotericsoftware.kryo.Registration import com.esotericsoftware.kryo.Serializer import net.corda.core.concurrent.CordaFuture import net.corda.core.CordaRuntimeException -import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory import net.corda.core.toFuture import net.corda.core.toObservable import net.corda.nodeapi.config.OldConfig @@ -49,7 +47,7 @@ class PermissionException(msg: String) : RuntimeException(msg) // The Kryo used for the RPC wire protocol. Every type in the wire protocol is listed here explicitly. // This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes, // because we can see everything we're using in one place. -class RPCKryo(observableSerializer: Serializer>, val serializationFactory: SerializationFactory, val serializationContext: SerializationContext) : CordaKryo(CordaClassResolver(serializationFactory, serializationContext)) { +class RPCKryo(observableSerializer: Serializer>, serializationContext: SerializationContext) : CordaKryo(CordaClassResolver(serializationContext)) { init { DefaultKryoCustomizer.customize(this) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 9d908e04bc..3bbcbcd028 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -21,10 +21,9 @@ fun Kryo.addToWhitelist(type: Class<*>) { } /** - * @param amqpEnabled Setting this to true turns on experimental AMQP serialization for any class annotated with - * [CordaSerializable]. + * Corda specific class resolver which enables extra customisation for the purposes of serialization using Kryo */ -class CordaClassResolver(val serializationFactory: SerializationFactory, val serializationContext: SerializationContext) : DefaultClassResolver() { +class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() { val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist) /** Returns the registration for the specified class, or null if the class is not registered. */ @@ -74,13 +73,11 @@ class CordaClassResolver(val serializationFactory: SerializationFactory, val ser val references = kryo.references try { kryo.references = true - val serializer = if (objectInstance != null) { - KotlinObjectSerializer(objectInstance) - } else if (kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type)) { - // Kotlin lambdas extend this class and any captured variables are stored in synthentic fields - FieldSerializer(kryo, type).apply { setIgnoreSyntheticFields(false) } - } else { - kryo.getDefaultSerializer(type) + val serializer = when { + objectInstance != null -> KotlinObjectSerializer(objectInstance) + kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type) -> // Kotlin lambdas extend this class and any captured variables are stored in synthetic fields + FieldSerializer(kryo, type).apply { setIgnoreSyntheticFields(false) } + else -> kryo.getDefaultSerializer(type) } return register(Registration(type, serializer, NAME.toInt())) } finally { @@ -117,11 +114,9 @@ class CordaClassResolver(val serializationFactory: SerializationFactory, val ser // TODO: come up with a more efficient way. e.g. segregate the name space by class loader. if (nameToClass != null) { val classesToRemove: MutableList = ArrayList(nameToClass.size) - for (entry in nameToClass.entries()) { - if (entry.value.classLoader is AttachmentsClassLoader) { - classesToRemove += entry.key - } - } + nameToClass.entries() + .filter { it.value.classLoader is AttachmentsClassLoader } + .forEach { classesToRemove += it.key } for (className in classesToRemove) { nameToClass.remove(className) } @@ -161,7 +156,7 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass } /** - * A whitelist that can be customised via the [CordaPluginRegistry], since implements [MutableClassWhitelist]. + * A whitelist that can be customised via the [net.corda.core.node.CordaPluginRegistry], since implements [MutableClassWhitelist]. */ class TransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index a6e51868a1..7ba674b671 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -68,11 +68,9 @@ open class SerializationFactoryImpl : SerializationFactory { private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): SerializationScheme { // truncate sequence to 8 bytes, and make sure it's a copy to avoid holding onto large ByteArrays return schemes.computeIfAbsent(byteSequence.take(HEADER_SIZE).copy() to target) { - for (scheme in registeredSchemes) { - if (scheme.canDeserializeVersion(it.first, it.second)) { - return@computeIfAbsent scheme - } - } + registeredSchemes + .filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) } + .forEach { return@computeIfAbsent it } NotSupportedSeralizationScheme } } @@ -119,7 +117,7 @@ private object AutoCloseableSerialisationDetector : Serializer() override fun read(kryo: Kryo, input: Input, type: Class) = throw IllegalStateException("Should not reach here!") } -abstract class AbstractKryoSerializationScheme(val serializationFactory: SerializationFactory) : SerializationScheme { +abstract class AbstractKryoSerializationScheme : SerializationScheme { private val kryoPoolsForContexts = ConcurrentHashMap, KryoPool>() protected abstract fun rpcClientKryoPool(context: SerializationContext): KryoPool @@ -131,7 +129,7 @@ abstract class AbstractKryoSerializationScheme(val serializationFactory: Seriali SerializationContext.UseCase.Checkpoint -> KryoPool.Builder { val serializer = Fiber.getFiberSerializer(false) as KryoSerializer - val classResolver = CordaClassResolver(serializationFactory, context).apply { setKryo(serializer.kryo) } + val classResolver = CordaClassResolver(context).apply { setKryo(serializer.kryo) } // TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true } serializer.kryo.apply { @@ -147,7 +145,7 @@ abstract class AbstractKryoSerializationScheme(val serializationFactory: Seriali rpcServerKryoPool(context) else -> KryoPool.Builder { - DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(serializationFactory, context))).apply { classLoader = it.second } + DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(context))).apply { classLoader = it.second } }.build() } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt index 88ac9f4049..11eb91de40 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt @@ -87,73 +87,73 @@ class CordaClassResolverTests { } - val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P) - val allButBlacklistedContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P) + private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P) + private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P) @Test fun `Annotation on enum works for specialised entries`() { // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. @Suppress("UNSUPPORTED_FEATURE") - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Foo.Bar::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) } @Test fun `Annotation on array element works`() { val values = arrayOf(Element()) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(values.javaClass) + CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) } @Test fun `Annotation not needed on abstract class`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(AbstractClass::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(AbstractClass::class.java) } @Test fun `Annotation not needed on interface`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Interface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(Interface::class.java) } @Test fun `Calling register method on modified Kryo does not consult the whitelist`() { - val kryo = CordaKryo(CordaClassResolver(factory, emptyWhitelistContext)) + val kryo = CordaKryo(CordaClassResolver(emptyWhitelistContext)) kryo.register(NotSerializable::class.java) } @Test(expected = KryoException::class) fun `Calling register method on unmodified Kryo does consult the whitelist`() { - val kryo = Kryo(CordaClassResolver(factory, emptyWhitelistContext), MapReferenceResolver()) + val kryo = Kryo(CordaClassResolver(emptyWhitelistContext), MapReferenceResolver()) kryo.register(NotSerializable::class.java) } @Test(expected = KryoException::class) fun `Annotation is needed without whitelisting`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(NotSerializable::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(NotSerializable::class.java) } @Test fun `Annotation is not needed with whitelisting`() { - val resolver = CordaClassResolver(factory, emptyWhitelistContext.withWhitelisted(NotSerializable::class.java)) + val resolver = CordaClassResolver(emptyWhitelistContext.withWhitelisted(NotSerializable::class.java)) resolver.getRegistration(NotSerializable::class.java) } @Test fun `Annotation not needed on Object`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Object::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(Object::class.java) } @Test fun `Annotation not needed on primitive`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Integer.TYPE) + CordaClassResolver(emptyWhitelistContext).getRegistration(Integer.TYPE) } @Test(expected = KryoException::class) fun `Annotation does not work for custom serializable`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(CustomSerializable::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(CustomSerializable::class.java) } @Test(expected = KryoException::class) fun `Annotation does not work in conjunction with Kryo annotation`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(DefaultSerializable::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(DefaultSerializable::class.java) } private fun importJar(storage: AttachmentStorage) = AttachmentClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) } @@ -164,23 +164,23 @@ class CordaClassResolverTests { val attachmentHash = importJar(storage) val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }) val attachedClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, classLoader) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(attachedClass) + CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass) } @Test fun `Annotation is inherited from interfaces`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SerializableViaInterface::class.java) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SerializableViaSubInterface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaInterface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaSubInterface::class.java) } @Test fun `Annotation is inherited from superclass`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SubElement::class.java) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SubSubElement::class.java) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SerializableViaSuperSubInterface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SubElement::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SubSubElement::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaSuperSubInterface::class.java) } - // Blacklist tests. + // Blacklist tests. Note: leave the variable public or else expected messages do not work correctly @get:Rule val expectedEx = ExpectedException.none()!! @@ -188,7 +188,7 @@ class CordaClassResolverTests { fun `Check blacklisted class`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("Class java.util.HashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // HashSet is blacklisted. resolver.getRegistration(HashSet::class.java) } @@ -198,7 +198,7 @@ class CordaClassResolverTests { fun `Check blacklisted subclass`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$SubHashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // SubHashSet extends the blacklisted HashSet. resolver.getRegistration(SubHashSet::class.java) } @@ -208,35 +208,35 @@ class CordaClassResolverTests { fun `Check blacklisted subsubclass`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$SubSubHashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // SubSubHashSet extends SubHashSet, which extends the blacklisted HashSet. resolver.getRegistration(SubSubHashSet::class.java) } - class ConnectionImpl(val connection: Connection) : Connection by connection + class ConnectionImpl(private val connection: Connection) : Connection by connection @Test fun `Check blacklisted interface impl`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$ConnectionImpl is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // ConnectionImpl implements blacklisted Connection. resolver.getRegistration(ConnectionImpl::class.java) } interface SubConnection : Connection - class SubConnectionImpl(val subConnection: SubConnection) : SubConnection by subConnection + class SubConnectionImpl(private val subConnection: SubConnection) : SubConnection by subConnection @Test fun `Check blacklisted super-interface impl`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$SubConnectionImpl is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // SubConnectionImpl implements SubConnection, which extends the blacklisted Connection. resolver.getRegistration(SubConnectionImpl::class.java) } @Test fun `Check forcibly allowed`() { - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // LinkedHashSet is allowed for serialization. resolver.getRegistration(LinkedHashSet::class.java) } @@ -247,7 +247,7 @@ class CordaClassResolverTests { fun `Check blacklist precedes CordaSerializable`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$CordaSerializableHashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // CordaSerializableHashSet is @CordaSerializable, but extends the blacklisted HashSet. resolver.getRegistration(CordaSerializableHashSet::class.java) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index bbe54aeb40..fe48e8d601 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -36,7 +36,7 @@ class KryoTests : TestDependencyInjectionBase() { @Before fun setup() { - factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, @@ -226,7 +226,7 @@ class KryoTests : TestDependencyInjectionBase() { } } Tmp() - val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt index 0ac1acdc18..2c8a337035 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt @@ -4,7 +4,6 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.io.Output import com.nhaarman.mockito_kotlin.mock -import net.corda.core.node.ServiceHub import net.corda.core.serialization.* import net.corda.core.utilities.OpaqueBytes import net.corda.testing.TestDependencyInjectionBase @@ -15,8 +14,8 @@ import java.io.ByteArrayOutputStream class SerializationTokenTest : TestDependencyInjectionBase() { - lateinit var factory: SerializationFactory - lateinit var context: SerializationContext + private lateinit var factory: SerializationFactory + private lateinit var context: SerializationContext @Before fun setup() { @@ -36,7 +35,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { override fun equals(other: Any?) = other is LargeTokenizable && other.bytes.size == this.bytes.size } - private fun serializeAsTokenContext(toBeTokenized: Any) = SerializeAsTokenContextImpl(toBeTokenized, factory, context, mock()) + private fun serializeAsTokenContext(toBeTokenized: Any) = SerializeAsTokenContextImpl(toBeTokenized, factory, context, mock()) @Test fun `write token and read tokenizable`() { @@ -91,7 +90,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(factory, this.context))) + val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(this.context))) val stream = ByteArrayOutputStream() Output(stream).use { it.write(KryoHeaderV0_1.bytes) diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 7d8fdda207..b30478e237 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -55,11 +55,10 @@ import kotlin.system.exitProcess * @param configuration This is typically loaded from a TypeSafe HOCON configuration file. * @param advertisedServices The services this node advertises. This must be a subset of the services it runs, * but nodes are not required to advertise services they run (hence subset). - * @param clock The clock used within the node and by all flows etc. */ open class Node(override val configuration: FullNodeConfiguration, advertisedServices: Set, - val versionInfo: VersionInfo, + private val versionInfo: VersionInfo, val initialiseSerialization: Boolean = true ) : AbstractNode(configuration, advertisedServices, createClock(configuration)) { companion object { @@ -128,7 +127,7 @@ open class Node(override val configuration: FullNodeConfiguration, // serialisation/deserialisation work. override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1) - var messageBroker: ArtemisMessagingServer? = null + private var messageBroker: ArtemisMessagingServer? = null private var shutdownHook: ShutdownHook? = null @@ -337,7 +336,7 @@ open class Node(override val configuration: FullNodeConfiguration, private fun initialiseSerialization() { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { - registerScheme(KryoServerSerializationScheme(this)) + registerScheme(KryoServerSerializationScheme()) registerScheme(AMQPServerSerializationScheme()) } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT diff --git a/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt index 14b5ef144e..fdaed4eebf 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt @@ -2,7 +2,6 @@ package net.corda.node.serialization import com.esotericsoftware.kryo.pool.KryoPool import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.ByteSequence import net.corda.node.services.messaging.RpcServerObservableSerializer import net.corda.nodeapi.RPCKryo @@ -10,7 +9,7 @@ import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1 -class KryoServerSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) { +class KryoServerSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return byteSequence == KryoHeaderV0_1 && target != SerializationContext.UseCase.RPCClient } @@ -21,7 +20,7 @@ class KryoServerSerializationScheme(serializationFactory: SerializationFactory) override fun rpcServerKryoPool(context: SerializationContext): KryoPool { return KryoPool.Builder { - DefaultKryoCustomizer.customize(RPCKryo(RpcServerObservableSerializer, serializationFactory, context)).apply { classLoader = context.deserializationClassLoader } + DefaultKryoCustomizer.customize(RPCKryo(RpcServerObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader } }.build() } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt index 69ada450ff..2c528cbfd5 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt @@ -398,7 +398,7 @@ class X509UtilitiesTest { @Test fun `serialize - deserialize X509CertififcateHolder`() { - val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, @@ -413,7 +413,7 @@ class X509UtilitiesTest { @Test fun `serialize - deserialize X509CertPath`() { - val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, diff --git a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt b/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt index 399bdcf922..859d8e24af 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/SerializationTestHelpers.kt @@ -61,8 +61,8 @@ fun initialiseTestSerialization() { // Now configure all the testing related delegates. (SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate = SerializationFactoryImpl().apply { - registerScheme(KryoClientSerializationScheme(this)) - registerScheme(KryoServerSerializationScheme(this)) + registerScheme(KryoClientSerializationScheme()) + registerScheme(KryoServerSerializationScheme()) registerScheme(AMQPClientSerializationScheme()) registerScheme(AMQPServerSerializationScheme()) } diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index 92c7a2c179..aaf0c3ac73 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -6,7 +6,6 @@ import com.typesafe.config.ConfigParseOptions import net.corda.core.internal.div import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.ByteSequence import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug @@ -89,13 +88,13 @@ class Verifier { private fun initialiseSerialization() { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { - registerScheme(KryoVerifierSerializationScheme(this)) + registerScheme(KryoVerifierSerializationScheme()) } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT } } - class KryoVerifierSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) { + class KryoVerifierSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return byteSequence == KryoHeaderV0_1 && target == SerializationContext.UseCase.P2P } From e49d3aa834642bea960d9af87ab659b700608000 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Thu, 17 Aug 2017 18:05:19 +0100 Subject: [PATCH 034/101] Use native VisibleForTesting everywhere. (#1189) --- .../net/corda/core/internal/concurrent/CordaFutureImpl.kt | 2 +- finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt | 2 +- node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt | 2 +- .../kotlin/net/corda/node/services/api/ServiceHubInternal.kt | 2 +- .../net/corda/node/services/network/InMemoryNetworkMapCache.kt | 2 +- .../kotlin/net/corda/node/services/network/NetworkMapService.kt | 2 +- .../net/corda/node/services/persistence/DBTransactionStorage.kt | 2 +- .../corda/node/services/persistence/NodeAttachmentService.kt | 2 +- .../corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt | 2 +- .../kotlin/net/corda/node/services/vault/NodeVaultService.kt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt index cc60db5386..e9a949d2e9 100644 --- a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt @@ -1,6 +1,6 @@ package net.corda.core.internal.concurrent -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.match import net.corda.core.utilities.getOrThrow diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 94fd65cb3a..73d76f5de7 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -1,6 +1,6 @@ package net.corda.contracts.asset -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.contracts.NetCommand import net.corda.contracts.NetType import net.corda.contracts.NettableState diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 914be297d7..84da8e024c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1,7 +1,7 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import com.google.common.collect.Lists import com.google.common.collect.MutableClassToInstanceMap import com.google.common.util.concurrent.MoreExecutors 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 2005010b83..192f4a0b55 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 @@ -1,6 +1,6 @@ package net.corda.node.services.api -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index 3af8f164e6..3ae68413d7 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -1,6 +1,6 @@ package net.corda.node.services.network -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.concurrent.CordaFuture import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.identity.AbstractParty diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index b5d5aa729b..0fed3ea12b 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -1,6 +1,6 @@ package net.corda.node.services.network -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.ThreadBox import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index fb637abd4c..0e68c98cce 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -1,6 +1,6 @@ package net.corda.node.services.persistence -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 7dac06f5a0..9b79915a13 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -1,7 +1,7 @@ package net.corda.node.services.persistence import com.codahale.metrics.MetricRegistry -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index 150143cc34..6d16a94db5 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -1,6 +1,6 @@ package net.corda.node.services.statemachine -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import com.google.common.primitives.Primitives import net.corda.core.flows.* import net.corda.core.serialization.CordaSerializable diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 88eb992172..03c61a1f76 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -2,7 +2,7 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import io.requery.PersistenceException import io.requery.kotlin.eq import io.requery.query.RowExpression From ae85759067c15bdc2c7e4856782e58e99518b0f4 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Fri, 18 Aug 2017 09:46:59 +0100 Subject: [PATCH 035/101] Added notifyVault flag to recordTransaction and skip notify for irrelevant TX (#1250) * Add notifyVault flag to recordTransaction to skip notifying the vault for transactions from ResolveTransactionFlow. * added methods for use in Java * reverted format changes * addressed PR issues changed recordTransaction method signature --- .../core/internal/ResolveTransactionsFlow.kt | 2 +- .../kotlin/net/corda/core/node/ServiceHub.kt | 29 ++++++++++++++----- .../net/corda/node/internal/AbstractNode.kt | 4 +-- .../node/services/api/ServiceHubInternal.kt | 10 ++++--- .../services/vault/HibernateVaultQueryImpl.kt | 1 - .../database/HibernateConfigurationTest.kt | 2 +- .../services/vault/NodeVaultServiceTest.kt | 5 ++-- .../net/corda/testing/node/MockServices.kt | 4 +-- 8 files changed, 37 insertions(+), 20 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 77fa327e91..e776b0efce 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -92,7 +92,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, // half way through, it's no big deal, although it might result in us attempting to re-download data // redundantly next time we attempt verification. it.verify(serviceHub) - serviceHub.recordTransactions(it) + serviceHub.recordTransactions(false, it) } return signedTransaction?.let { diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 0a885130d9..fb882eb4ad 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -68,18 +68,35 @@ interface ServiceHub : ServicesForResolution { /** * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for - * further processing. This is expected to be run within a database transaction. + * further processing if [notifyVault] is true. This is expected to be run within a database transaction. * * @param txs The transactions to record. + * @param notifyVault indicate if the vault should be notified for the update. */ - fun recordTransactions(txs: Iterable) + fun recordTransactions(notifyVault: Boolean, txs: Iterable) + + /** + * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for + * further processing if [notifyVault] is true. This is expected to be run within a database transaction. + */ + fun recordTransactions(notifyVault: Boolean, first: SignedTransaction, vararg remaining: SignedTransaction) { + recordTransactions(notifyVault, listOf(first, *remaining)) + } /** * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for * further processing. This is expected to be run within a database transaction. */ fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) { - recordTransactions(listOf(first, *remaining)) + recordTransactions(true, first, *remaining) + } + + /** + * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for + * further processing. This is expected to be run within a database transaction. + */ + fun recordTransactions(txs: Iterable) { + recordTransactions(true, txs) } /** @@ -92,8 +109,7 @@ interface ServiceHub : ServicesForResolution { val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) return if (stx.isNotaryChangeTransaction()) { stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index] - } - else stx.tx.outputs[stateRef.index] + } else stx.tx.outputs[stateRef.index] } /** @@ -106,8 +122,7 @@ interface ServiceHub : ServicesForResolution { val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) return if (stx.isNotaryChangeTransaction()) { stx.resolveNotaryChangeTransaction(this).outRef(stateRef.index) - } - else { + } else { stx.tx.outRef(stateRef.index) } } diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 84da8e024c..4b5db6c46a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -801,9 +801,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return flowFactories[initiatingFlowClass] } - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { database.transaction { - super.recordTransactions(txs) + super.recordTransactions(notifyVault, txs) } } override fun jdbcSession(): Connection = database.createSession() 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 192f4a0b55..a3989324a0 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 @@ -89,8 +89,8 @@ interface ServiceHubInternal : PluginServiceHub { val database: CordaPersistence val configuration: NodeConfiguration - override fun recordTransactions(txs: Iterable) { - require (txs.any()) { "No transactions passed in for recording" } + override fun recordTransactions(notifyVault: Boolean, 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) { @@ -101,8 +101,10 @@ interface ServiceHubInternal : PluginServiceHub { log.warn("Transactions recorded from outside of a state machine") } - val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } - vaultService.notifyAll(toNotify) + if (notifyVault) { + val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } + vaultService.notifyAll(toNotify) + } } /** diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index a8a58bd2a8..55cd451b38 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -28,7 +28,6 @@ import java.lang.Exception import java.util.* import javax.persistence.Tuple - class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val vault: VaultService) : SingletonSerializeAsToken(), VaultQueryService { companion object { diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index c9abfefbe1..1f9c6de17b 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -75,7 +75,7 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } 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 62e1591b15..91587226b2 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 @@ -94,13 +94,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val originalVaultQuery = vaultQuery val services2 = object : MockServices() { override val vaultService: VaultService get() = originalVault - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) vaultService.notify(stx.tx) } } - override val vaultQueryService : VaultQueryService get() = originalVaultQuery + + override val vaultQueryService: VaultQueryService get() = originalVaultQuery } val w2 = services2.vaultQueryService.queryBy().states diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index c468025bda..bffbf0ea2a 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -60,7 +60,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { val key: KeyPair get() = keys.first() - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { txs.forEach { stateMachineRecordedTransactionMapping.addMapping(StateMachineRunId.createRandom(), it.id) } @@ -229,7 +229,7 @@ fun makeTestDatabaseAndMockServices(customSchemas: Set = setOf(Com object : MockServices(*(keys.toTypedArray())) { override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } From 5864a22d05e9bed9c273c3ea3ebeb6c5a9ff4aeb Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Fri, 18 Aug 2017 09:17:03 +0100 Subject: [PATCH 036/101] Move dependency on "proton-j" library from "core" sub-project to "node-api" --- core/build.gradle | 3 --- node-api/build.gradle | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/build.gradle b/core/build.gradle index edf8fb1037..657618f742 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -66,9 +66,6 @@ dependencies { // Requery: SQL based query & persistence for Kotlin compile "io.requery:requery-kotlin:$requery_version" - - // For AMQP serialisation. - compile "org.apache.qpid:proton-j:0.19.0" } configurations { diff --git a/node-api/build.gradle b/node-api/build.gradle index 43d289940b..a0844f5f13 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -31,6 +31,9 @@ dependencies { compile "com.esotericsoftware:kryo:4.0.0" compile "de.javakaffee:kryo-serializers:0.41" + // For AMQP serialisation. + compile "org.apache.qpid:proton-j:0.19.0" + // Unit testing helpers. testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" From ec83735ea372be81dbaf1eed567432ce77bdf508 Mon Sep 17 00:00:00 2001 From: mkit Date: Fri, 18 Aug 2017 10:16:16 +0100 Subject: [PATCH 037/101] Adding hint on how to fix the 'not instrumented' error message (#1274) * Adding hint on how to fix the 'not instrumented' error message * Addressing review comments --- .../node/services/statemachine/StateMachineManager.kt | 9 +++++++++ .../src/main/kotlin/net/corda/testing/driver/Driver.kt | 3 --- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index fbbbbaf4fd..4c2e3b0368 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.FiberExecutorScheduler +import co.paralleluniverse.fibers.instrument.SuspendableHelper import co.paralleluniverse.strands.Strand import com.codahale.metrics.Gauge import com.esotericsoftware.kryo.KryoException @@ -166,11 +167,19 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val changes: Observable = mutex.content.changesPublisher.wrapWithDatabaseTransaction() fun start() { + checkQuasarJavaAgentPresence() restoreFibersFromCheckpoints() listenToLedgerTransactions() serviceHub.networkMapCache.mapServiceRegistered.then { executor.execute(this::resumeRestoredFibers) } } + private fun checkQuasarJavaAgentPresence() { + check(SuspendableHelper.isJavaAgentActive(), { + """Missing the '-javaagent' JVM argument. Make sure you run the tests with the Quasar java agent attached to your JVM. + #See https://docs.corda.net/troubleshooting.html - 'Fiber classes not instrumented' for more details.""".trimMargin("#") + }) + } + private fun listenToLedgerTransactions() { // Observe the stream of committed, validated transactions and resume fibers that are waiting for them. serviceHub.validatedTransactions.updates.subscribe { stx -> diff --git a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index 44f3ad88e3..fb99b80c6c 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -26,12 +26,10 @@ import net.corda.core.node.services.ServiceType import net.corda.core.utilities.* import net.corda.node.internal.Node import net.corda.node.internal.NodeStartup -import net.corda.node.serialization.NodeClock import net.corda.node.services.config.* import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator -import net.corda.node.utilities.TestClock import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration @@ -47,7 +45,6 @@ import java.io.File import java.net.* import java.nio.file.Path import java.nio.file.Paths -import java.time.Clock import java.time.Duration import java.time.Instant import java.time.ZoneOffset.UTC From d22cdac2ddd51b7749b17351eacb28ba5fb1a1f7 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 17 Aug 2017 13:12:20 +0200 Subject: [PATCH 038/101] Move some extension methods for summing to new locations. This improves the Java API and makes it more idiomatic. The methods were not moved to be static methods of the relevant types in all cases due to a bad interaction with a Kotlin auto-completion bug, and because static methods on interfaces are new in Java 8 and Kotlin is not yet emitting Java 8 bytecode. Also, introduce a packages.md file so packages can be documented. --- .../kotlin/net/corda/core/contracts/Amount.kt | 94 ++++++++++++------- .../net/corda/core/contracts/FungibleAsset.kt | 11 +-- .../net/corda/core/contracts/AmountTests.kt | 1 + docs/build.gradle | 1 + docs/packages.md | 3 + docs/source/changelog.rst | 6 ++ .../corda/contracts/JavaCommercialPaper.java | 9 +- .../net/corda/contracts/CommercialPaper.kt | 11 +-- .../kotlin/net/corda/contracts/asset/Cash.kt | 44 +++------ .../contracts/asset/CommodityContract.kt | 17 +--- .../net/corda/contracts/asset/Obligation.kt | 17 +--- .../corda/contracts/asset/OnLedgerAsset.kt | 1 + .../finance/utils/StateSummingUtilities.kt | 73 ++++++++++++++ .../net/corda/flows/TwoPartyTradeFlow.kt | 2 +- .../corda/contracts/asset/CashTestsJava.java | 2 +- .../corda/contracts/CommercialPaperTests.kt | 2 +- .../corda/contracts/DummyFungibleContract.kt | 3 + .../net/corda/contracts/asset/CashTests.kt | 4 + .../services/vault/VaultQueryJavaTests.java | 10 +- .../database/HibernateConfigurationTest.kt | 1 + .../services/vault/NodeVaultServiceTest.kt | 2 +- .../views/cordapps/cash/NewTransaction.kt | 2 +- 22 files changed, 195 insertions(+), 121 deletions(-) create mode 100644 docs/packages.md create mode 100644 finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index d74ea62be3..977e936eb7 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -13,6 +13,7 @@ import java.util.* * indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects. */ interface TokenizableAssetInfo { + /** The nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. */ val displayTokenSize: BigDecimal } @@ -28,16 +29,14 @@ interface TokenizableAssetInfo { * multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer * overflow. * - * @param quantity the number of tokens as a Long value. - * @param displayTokenSize the nominal display unit size of a single token, - * potentially with trailing decimal display places if the scale parameter is non-zero. - * @param T the type of the token, for example [Currency]. - * T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required. - * - * TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application. + * @property quantity the number of tokens as a Long value. + * @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. + * @property token an instance of type T, usually a singleton. + * @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required. */ @CordaSerializable data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable> { + // TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application. companion object { /** * Build an Amount from a decimal representation. For example, with an input of "12.34 GBP", @@ -73,6 +72,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * For other possible token types the asset token should implement TokenizableAssetInfo to * correctly report the designed nominal amount. */ + @JvmStatic fun getDisplayTokenSize(token: Any): BigDecimal { if (token is TokenizableAssetInfo) { return token.displayTokenSize @@ -85,6 +85,28 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, } return BigDecimal.ONE } + + /** + * If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if + * any of the token types are mismatched; if the iterator yields no elements, return null. + */ + @JvmStatic + fun Iterable>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow() + + /** + * Sums the amounts yielded by the given iterable, throwing an [IllegalArgumentException] if any of the token + * types are mismatched. + */ + @JvmStatic + fun Iterable>.sumOrThrow() = reduce { left, right -> left + right } + + /** + * If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if + * any of the token types are mismatched; if the iterator yields no elements, return a zero amount of the given + * token type. + */ + @JvmStatic + fun Iterable>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token) } init { @@ -106,8 +128,9 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, /** * A checked addition operator is supported to simplify aggregation of Amounts. + * Mixing non-identical token types will throw [IllegalArgumentException]. + * * @throws ArithmeticException if there is overflow of Amount tokens during the summation - * Mixing non-identical token types will throw [IllegalArgumentException] */ operator fun plus(other: Amount): Amount { checkToken(other) @@ -117,8 +140,9 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, /** * A checked addition operator is supported to simplify netting of Amounts. * If this leads to the Amount going negative this will throw [IllegalArgumentException]. + * Mixing non-identical token types will throw [IllegalArgumentException]. + * * @throws ArithmeticException if there is Numeric underflow - * Mixing non-identical token types will throw [IllegalArgumentException] */ operator fun minus(other: Amount): Amount { checkToken(other) @@ -137,6 +161,11 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, */ operator fun times(other: Long): Amount = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token) + /** + * The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount. + * Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour. + * N.B. Division is not supported as fractional tokens are not representable by an Amount. + */ operator fun times(other: Int): Amount = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token) /** @@ -159,7 +188,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize, * which determines the size of a single token and controls the trailing decimal places via it's scale property. * - * @see Amount.Companion.fromDecimal + * @see Amount.fromDecimal */ fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize @@ -171,29 +200,27 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * The result of fromDecimal is used to control the numerical formatting and * the token specifier appended is taken from token.toString. * - * @see Amount.Companion.fromDecimal + * @see Amount.fromDecimal */ override fun toString(): String { return toDecimal().toPlainString() + " " + token } + /** @suppress */ override fun compareTo(other: Amount): Int { checkToken(other) return quantity.compareTo(other.quantity) } } - -fun Iterable>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow() -fun Iterable>.sumOrThrow() = reduce { left, right -> left + right } -fun Iterable>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token) - - /** * Simple data class to associate the origin, owner, or holder of a particular Amount object. - * @param source the holder of the Amount. - * @param amount the Amount of asset available. - * @param ref is an optional field used for housekeeping in the caller. + * + * @param P Any class type that can disambiguate where the amount came from. + * @param T The token type of the underlying [Amount]. + * @property source the holder of the Amount. + * @property amount the Amount of asset available. + * @property ref is an optional field used for housekeeping in the caller. * e.g. to point back at the original Vault state objects. * @see SourceAndAmount.apply which processes a list of SourceAndAmount objects * and calculates the resulting Amount distribution as a new list of SourceAndAmount objects. @@ -203,17 +230,17 @@ data class SourceAndAmount(val source: P, val amount: Amou /** * This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date. * - * @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then + * @property quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then * it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta) * at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens * and the destination will lose Math.abs(quantityDelta) tokens. * Where possible the source and destination should be coded to ensure a positive quantityDelta, * but in various scenarios it may be more consistent to allow positive and negative values. * For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination. - * @param token represents the type of asset token as would be used to construct Amount objects. - * @param source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive, + * @property token represents the type of asset token as would be used to construct Amount objects. + * @property source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive, * or the token sink if quantityDelta is negative. The type P should support value equality. - * @param destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive, + * @property destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive, * or the token source if quantityDelta is negative. The type P should support value equality. */ @CordaSerializable @@ -245,9 +272,7 @@ class AmountTransfer(val quantityDelta: Long, return AmountTransfer(deltaTokenCount, token, source, destination) } - /** - * Helper to make a zero size AmountTransfer - */ + /** Helper to make a zero size AmountTransfer. */ @JvmStatic fun zero(token: T, source: P, @@ -284,6 +309,7 @@ class AmountTransfer(val quantityDelta: Long, */ fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token) + /** @suppress */ fun copy(quantityDelta: Long = this.quantityDelta, token: T = this.token, source: P = this.source, @@ -313,7 +339,7 @@ class AmountTransfer(val quantityDelta: Long, } /** - * HashCode ensures that reversed source and destination equivalents will hash to the same value. + * This hash code function ensures that reversed source and destination equivalents will hash to the same value. */ override fun hashCode(): Int { var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values @@ -322,18 +348,20 @@ class AmountTransfer(val quantityDelta: Long, return result } + /** @suppress */ override fun toString(): String { return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token" } /** - * Novation is a common financial operation in which a bilateral exchange is modified so that the same - * relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house. + * Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. The net + * total exchange is the same as in the original input. Novation is a common financial operation in which a + * bilateral exchange is modified so that the same relative asset exchange happens, but with each party exchanging + * versus a central counterparty, or clearing house. * * @param centralParty The central party to face the exchange against. - * @return Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. - * The net total exchange is the same as in the original input. */ + @Suppress("UNUSED") fun novate(centralParty: P): List> = listOf(copy(destination = centralParty), copy(source = centralParty)) /** @@ -343,7 +371,7 @@ class AmountTransfer(val quantityDelta: Long, * @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange. * @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output. * i.e. To the new payment destination entry and to any residual change output. - * @return The returned list is a copy of the original list, except that funds needed to cover the exchange + * @return A copy of the original list, except that funds needed to cover the exchange * will have been removed and a new output and possibly residual amount entry will be added at the end of the list. * @throws ArithmeticException if there is underflow in the summations. */ diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index bf6d856d97..dcc2897ad0 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -3,6 +3,8 @@ package net.corda.core.contracts import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import java.security.PublicKey +import net.corda.core.contracts.Amount.Companion.sumOrNull +import net.corda.core.contracts.Amount.Companion.sumOrZero class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing") @@ -51,12 +53,3 @@ interface FungibleAsset : OwnableState { } } } - -// Small DSL extensions. - -/** Sums the asset states in the list, returning null if there are none. */ -fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() - -/** Sums the asset states in the list, returning zero of the given token if there are none. */ -fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) - diff --git a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt index 1a39b9ed49..45923f2238 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt @@ -1,6 +1,7 @@ package net.corda.core.contracts import net.corda.finance.* +import net.corda.core.contracts.Amount.Companion.sumOrZero import org.junit.Test import java.math.BigDecimal import java.util.* diff --git a/docs/build.gradle b/docs/build.gradle index 8b2cdf5a1e..b48623ac83 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -18,6 +18,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc") processConfigurations = ['compile'] sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') + includes = ['packages.md'] } task buildDocs(dependsOn: ['apidocs', 'makeDocs']) diff --git a/docs/packages.md b/docs/packages.md new file mode 100644 index 0000000000..6c620dede6 --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,3 @@ +# Package net.corda.finance.utils + +A collection of utilities for summing financial states, for example, summing obligations to get total debts. \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index a5d12f5e78..4cc46a138c 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -45,6 +45,12 @@ UNRELEASED * Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use `CashIssueFlow` instead. +* Some utility/extension functions (``sumOrThrow``, ``sumOrNull``, ``sumOrZero`` on ``Amount`` and ``Commodity``) + have moved to be static methods on the classes themselves. This improves the API for Java users who no longer + have to see or known about file-level FooKt style classes generated by the Kotlin compile, but means that IntelliJ + no longer auto-suggests these extension functions in completion unless you add import lines for them yourself + (this is Kotlin IDE bug KT-15286). + Milestone 14 ------------ diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index d4910e67a9..88e3de772c 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -3,10 +3,8 @@ package net.corda.contracts; import co.paralleluniverse.fibers.Suspendable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import kotlin.Pair; -import kotlin.Unit; -import net.corda.contracts.asset.Cash; -import net.corda.contracts.asset.CashKt; +import kotlin.*; +import net.corda.contracts.asset.*; import net.corda.core.contracts.*; import net.corda.core.crypto.SecureHash; import net.corda.core.crypto.testing.NullPublicKey; @@ -16,6 +14,7 @@ import net.corda.core.identity.Party; import net.corda.core.node.ServiceHub; import net.corda.core.transactions.LedgerTransaction; import net.corda.core.transactions.TransactionBuilder; +import net.corda.finance.utils.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -207,7 +206,7 @@ public class JavaCommercialPaper implements Contract { final Instant time = null == timeWindow ? null : timeWindow.getUntilTime(); - final Amount> received = CashKt.sumCashBy(tx.getOutputs().stream().map(TransactionState::getData).collect(Collectors.toList()), input.getOwner()); + final Amount> received = StateSumming.sumCashBy(tx.getOutputStates(), input.getOwner()); requireThat(require -> { require.using("must be timestamped", timeWindow != null); diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index f94ff09dd5..49ddf736dd 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -2,7 +2,6 @@ package net.corda.contracts import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.testing.NULL_PARTY @@ -16,6 +15,7 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.utils.sumCashBy import net.corda.schemas.CommercialPaperSchemaV1 import java.time.Instant import java.util.* @@ -88,6 +88,9 @@ class CommercialPaper : Contract { else -> throw IllegalArgumentException("Unrecognised schema $schema") } } + + /** @suppress */ infix fun `owned by`(owner: AbstractParty) = copy(owner = owner) + /** @suppress */ infix fun `with notary`(notary: Party) = TransactionState(this, notary) } interface Commands : CommandData { @@ -191,8 +194,4 @@ class CommercialPaper : Contract { tx.addInputState(paper) tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey) } -} - -infix fun CommercialPaper.State.`owned by`(owner: AbstractParty) = copy(owner = owner) -infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary) -infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner) \ No newline at end of file +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 7d4887cc5a..97482b138b 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -1,3 +1,4 @@ +@file:JvmName("CashUtilities") // So the static extension functions get put into a class with a better name than CashKt package net.corda.contracts.asset import co.paralleluniverse.fibers.Suspendable @@ -25,6 +26,9 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toHexString import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.trace +import net.corda.finance.utils.sumCash +import net.corda.finance.utils.sumCashOrNull +import net.corda.finance.utils.sumCashOrZero import net.corda.schemas.CashSchemaV1 import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger @@ -94,6 +98,10 @@ class Cash : OnLedgerAsset() { override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)" override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner)) + fun ownedBy(owner: AbstractParty) = copy(owner = owner) + fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) + fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) + fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) /** Object Relational Mapping support. */ override fun generateMappedObject(schema: MappedSchema): PersistentState { @@ -278,7 +286,7 @@ class Cash : OnLedgerAsset() { * otherwise the set of eligible states wil be filtered to only include those from these issuers. * @param notary If null the notary source is ignored, if specified then only states marked * with this notary are included. - * @param lockId The [FlowLogic.runId.uuid] of the flow, which is used to soft reserve the states. + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. * @param withIssuerRefs If not empty the specific set of issuer references to match against. * @return The matching states that were found. If sufficient funds were found these will be locked, @@ -387,36 +395,10 @@ class Cash : OnLedgerAsset() { // Small DSL extensions. -/** - * Sums the cash states in the list belonging to a single owner, throwing an exception - * if there are none, or if any of the cash states cannot be added together (i.e. are - * different currencies or issuers). - */ -fun Iterable.sumCashBy(owner: AbstractParty): Amount> = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() - -/** - * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash - * states cannot be added together (i.e. are different currencies or issuers). - */ -fun Iterable.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() - -/** Sums the cash states in the list, returning null if there are none. */ -fun Iterable.sumCashOrNull(): Amount>? = filterIsInstance().map { it.amount }.sumOrNull() - -/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */ -fun Iterable.sumCashOrZero(currency: Issued): Amount> { - return filterIsInstance().map { it.amount }.sumOrZero(currency) -} - -fun Cash.State.ownedBy(owner: AbstractParty) = copy(owner = owner) -fun Cash.State.issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) -fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) -fun Cash.State.withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) - -infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) -infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) -infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) +/** @suppress */ infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) +/** @suppress */ infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) +/** @suppress */ infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +/** @suppress */ infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) // Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt index f9f0a13523..26a2198adb 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt @@ -9,6 +9,9 @@ import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.utils.sumCommodities +import net.corda.finance.utils.sumCommoditiesOrNull +import net.corda.finance.utils.sumCommoditiesOrZero import java.util.* ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -178,16 +181,4 @@ class CommodityContract : OnLedgerAsset>) = Commands.Exit(amount) override fun generateIssueCommand() = Commands.Issue() override fun generateMoveCommand() = Commands.Move() -} - -/** - * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash - * states cannot be added together (i.e. are different currencies). - */ -fun Iterable.sumCommodities() = filterIsInstance().map { it.amount }.sumOrThrow() - -/** Sums the cash states in the list, returning null if there are none. */ -@Suppress("unused") fun Iterable.sumCommoditiesOrNull() = filterIsInstance().map { it.amount }.sumOrNull() - -/** Sums the cash states in the list, returning zero of the given currency if there are none. */ -fun Iterable.sumCommoditiesOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero(currency) +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index 73d76f5de7..de30086bd5 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -18,6 +18,10 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.seconds +import net.corda.finance.utils.sumFungibleOrNull +import net.corda.finance.utils.sumObligations +import net.corda.finance.utils.sumObligationsOrNull +import net.corda.finance.utils.sumObligationsOrZero import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.PublicKey @@ -71,7 +75,6 @@ val OBLIGATION_PROGRAM_ID = Obligation() * @param P the product the obligation is for payment of. */ class Obligation

: Contract { - /** * TODO: * 1) hash should be of the contents, not the URI @@ -792,18 +795,6 @@ fun

sumAmountsDue(balances: Map, Amount< return sum } -/** Sums the obligation states in the list, throwing an exception if there are none. All state objects in the list are presumed to be nettable. */ -fun

Iterable.sumObligations(): Amount>> - = filterIsInstance>().map { it.amount }.sumOrThrow() - -/** Sums the obligation states in the list, returning null if there are none. */ -fun

Iterable.sumObligationsOrNull(): Amount>>? - = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull() - -/** Sums the obligation states in the list, returning zero of the given product if there are none. */ -fun

Iterable.sumObligationsOrZero(issuanceDef: Issued>): Amount>> - = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef) - infix fun Obligation.State.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore)) infix fun Obligation.State.between(parties: Pair) = copy(obligor = parties.first, beneficiary = parties.second) infix fun Obligation.State.`owned by`(owner: AbstractParty) = copy(beneficiary = owner) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt index c40649b789..050d3a86da 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt @@ -1,6 +1,7 @@ package net.corda.contracts.asset import net.corda.core.contracts.* +import net.corda.core.contracts.Amount.Companion.sumOrThrow import net.corda.core.identity.AbstractParty import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.loggerFor diff --git a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt new file mode 100644 index 0000000000..18e30180f2 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt @@ -0,0 +1,73 @@ +@file:JvmName("StateSumming") +package net.corda.finance.utils + +import net.corda.contracts.Commodity +import net.corda.contracts.asset.Cash +import net.corda.contracts.asset.CommodityContract +import net.corda.contracts.asset.Obligation +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Amount.Companion.sumOrNull +import net.corda.core.contracts.Amount.Companion.sumOrThrow +import net.corda.core.contracts.Amount.Companion.sumOrZero +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.Issued +import net.corda.core.identity.AbstractParty +import java.util.* + +/** A collection of utilities for summing states */ + +/** + * Sums the cash states in the list belonging to a single owner, throwing an exception + * if there are none, or if any of the cash states cannot be added together (i.e. are + * different currencies or issuers). + */ +fun Iterable.sumCashBy(owner: AbstractParty): Amount> = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() + +/** + * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash + * states cannot be added together (i.e. are different currencies or issuers). + */ +fun Iterable.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() + +/** Sums the cash states in the list, returning null if there are none. */ +fun Iterable.sumCashOrNull(): Amount>? = filterIsInstance().map { it.amount }.sumOrNull() + +/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */ +fun Iterable.sumCashOrZero(currency: Issued): Amount> { + return filterIsInstance().map { it.amount }.sumOrZero(currency) +} + +/** Sums the asset states in the list, returning null if there are none. */ +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() + +/** Sums the asset states in the list, returning zero of the given token if there are none. */ +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) + +/** + * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash + * states cannot be added together (i.e. are different currencies). + */ +fun Iterable.sumCommodities() = filterIsInstance().map { it.amount }.sumOrThrow() + +/** Sums the cash states in the list, returning null if there are none. */ +@Suppress("unused") +fun Iterable.sumCommoditiesOrNull() = filterIsInstance().map { it.amount }.sumOrNull() + +/** Sums the cash states in the list, returning zero of the given currency if there are none. */ +fun Iterable.sumCommoditiesOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero(currency) + +/** + * Sums the obligation states in the list, throwing an exception if there are none. All state objects in the + * list are presumed to be nettable. + */ +fun

Iterable.sumObligations(): Amount>> + = filterIsInstance>().map { it.amount }.sumOrThrow() + +/** Sums the obligation states in the list, returning null if there are none. */ +fun

Iterable.sumObligationsOrNull(): Amount>>? + = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrNull() + +/** Sums the obligation states in the list, returning zero of the given product if there are none. */ +fun

Iterable.sumObligationsOrZero(issuanceDef: Issued>): Amount>> + = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef) diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index e63be156e3..b85b948976 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -2,7 +2,6 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.Amount import net.corda.core.contracts.OwnableState import net.corda.core.contracts.StateAndRef @@ -18,6 +17,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.seconds import net.corda.core.utilities.unwrap +import net.corda.finance.utils.sumCashBy import java.security.PublicKey import java.util.* diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index 4b7f2e07a7..5d2a696f35 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -45,7 +45,7 @@ public class CashTestsJava { tw.output(outState); // issuedBy() can't be directly imported because it conflicts with other identically named functions // with different overloads (for some reason). - tw.output(CashKt.issuedBy(outState, getMINI_CORP())); + tw.output(outState.issuedBy(getMINI_CORP())); tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); return tw.failsWith("at least one cash input"); }); diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 9bc8c2beb7..5f2592164d 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -107,7 +107,7 @@ class CommercialPaperTestsGeneric { input("paper") input("alice's $900") output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } - output("alice's paper") { "paper".output() `owned by` ALICE } + output("alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } this.verifies() diff --git a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt index 4432189ee2..e1c9274628 100644 --- a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt +++ b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt @@ -12,6 +12,9 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.utils.sumCash +import net.corda.finance.utils.sumCashOrNull +import net.corda.finance.utils.sumCashOrZero import net.corda.schemas.SampleCashSchemaV1 import net.corda.schemas.SampleCashSchemaV2 import net.corda.schemas.SampleCashSchemaV3 diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 7c2d05c3c0..c9613a1f03 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -12,6 +12,10 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.finance.* +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.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index f779d62273..52d3169f08 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -3,7 +3,7 @@ package net.corda.node.services.vault; import com.google.common.collect.ImmutableSet; import kotlin.Pair; import net.corda.contracts.DealState; -import net.corda.contracts.asset.Cash; +import net.corda.contracts.asset.*; import net.corda.core.contracts.*; import net.corda.core.crypto.EncodingUtils; import net.corda.core.identity.AbstractParty; @@ -34,13 +34,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; -import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM; import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; import static net.corda.core.utilities.ByteArrays.toHexString; import static net.corda.testing.CoreTestUtils.*; import static net.corda.testing.TestConstants.*; +import static net.corda.contracts.asset.CashUtilities.*; import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices; import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; import static org.assertj.core.api.Assertions.assertThat; @@ -57,7 +56,6 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); keys.add(getDUMMY_NOTARY_KEY()); - IdentityService identitySvc = makeTestIdentityService(); Pair databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys, () -> identitySvc); issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); @@ -127,7 +125,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { Amount amount = new Amount<>(100, Currency.getInstance("USD")); VaultFiller.fillWithSomeTestCash(services, - new Amount<>(100, Currency.getInstance("USD")), + new Amount(100, Currency.getInstance("USD")), issuerServices, TestConstants.getDUMMY_NOTARY(), 3, @@ -135,7 +133,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { new Random(), new OpaqueBytes("1".getBytes()), null, - getDUMMY_CASH_ISSUER()); + CashUtilities.getDUMMY_CASH_ISSUER()); VaultFiller.consumeCash(services, amount, getDUMMY_NOTARY()); diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 1f9c6de17b..c5050f3fc8 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -13,6 +13,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.finance.DOLLARS import net.corda.finance.POUNDS import net.corda.finance.SWISS_FRANCS +import net.corda.finance.utils.sumCash import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.VaultSchemaV1 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 91587226b2..83e81255dc 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 @@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.contracts.asset.sumCash import net.corda.contracts.getCashBalance import net.corda.core.contracts.* import net.corda.core.crypto.generateKeyPair @@ -21,6 +20,7 @@ import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet import net.corda.finance.* +import net.corda.finance.utils.sumCash import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index bb5a9fa07a..ba13898e85 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -18,7 +18,7 @@ import net.corda.client.jfx.utils.isNotNull import net.corda.client.jfx.utils.map import net.corda.client.jfx.utils.unique import net.corda.core.contracts.Amount -import net.corda.core.contracts.sumOrNull +import net.corda.core.contracts.Amount.Companion.sumOrNull import net.corda.core.contracts.withoutIssuer import net.corda.core.flows.FlowException import net.corda.core.identity.Party From 2829faa01f5c6dfdbf7e943d16219282bbd3dd01 Mon Sep 17 00:00:00 2001 From: Andrzej Cichocki Date: Fri, 18 Aug 2017 13:30:39 +0100 Subject: [PATCH 039/101] Retire legalContractReference (#1188) * Add LegalProseReference annotation * Migrate code from autogenerated javascript to TypeScript source * Add instructions to rebuild the web resources * Make installWeb more reproducible --- .../net/corda/core/contracts/Structures.kt | 17 +- .../contracts/TransactionEncumbranceTests.kt | 2 - .../core/flows/ContractUpgradeFlowTest.kt | 4 - .../net/corda/core/node/VaultUpdateTests.kt | 2 - .../TransactionSerializationTests.kt | 3 - docs/source/api-contracts.rst | 21 +- .../docs/WorkflowTransactionBuildTutorial.kt | 5 +- docs/source/hello-world-contract.rst | 15 +- docs/source/hello-world-running.rst | 3 +- docs/source/tutorial-contract-clauses.rst | 7 - docs/source/tutorial-contract.rst | 14 +- docs/source/tutorial-cordapp.rst | 6 +- .../contracts/universal/UniversalContract.kt | 4 - .../isolated/AnotherDummyContract.kt | 4 - .../corda/contracts/JavaCommercialPaper.java | 12 +- .../net/corda/contracts/CommercialPaper.kt | 4 - .../kotlin/net/corda/contracts/asset/Cash.kt | 19 +- .../contracts/asset/CommodityContract.kt | 20 +- .../net/corda/contracts/asset/Obligation.kt | 43 +-- .../corda/contracts/DummyFungibleContract.kt | 5 +- .../corda/contracts/asset/ObligationTests.kt | 57 ++-- .../nodeapi/AttachmentClassLoaderTests.kt | 8 +- .../amqp/SerializationOutputTests.kt | 2 - .../services/vault/schemas/VaultSchemaTest.kt | 2 - .../corda/attachmentdemo/AttachmentDemo.kt | 3 - .../main/kotlin/net/corda/irs/contract/IRS.kt | 3 - .../notarydemo/flows/DummyIssueAndMove.kt | 4 +- samples/simm-valuation-demo/README.md | 7 +- samples/simm-valuation-demo/build.gradle | 9 +- .../net/corda/vega/api/PortfolioApiUtils.kt | 2 - .../net/corda/vega/contracts/OGTrade.kt | 3 +- .../net/corda/vega/contracts/PortfolioSwap.kt | 3 +- .../simmvaluationweb/app/Deal.js.map | 2 +- .../simmvaluationweb/app/app.component.js | 12 +- .../simmvaluationweb/app/app.component.js.map | 2 +- .../app/app.component.spec.js.map | 2 +- .../simmvaluationweb/app/app.routes.js.map | 2 +- .../create-trade.component.js.map | 2 +- .../create-trade.component.spec.js.map | 2 +- .../app/create-trade/index.js.map | 2 +- .../app/create-trade/shared/index.js.map | 2 +- .../simmvaluationweb/app/environment.js.map | 2 +- .../app/http-wrapper.service.js.map | 2 +- .../app/http-wrapper.service.spec.js.map | 2 +- .../simmvaluationweb/app/index.js.map | 2 +- .../simmvaluationweb/app/irs.service.js.map | 2 +- .../app/irs.service.spec.js.map | 2 +- .../simmvaluationweb/app/model/CommonModel.js | 1 - .../app/model/CommonModel.js.map | 2 +- .../app/model/FixedLegModel.js.map | 2 +- .../app/model/FloatingLegModel.js.map | 2 +- .../simmvaluationweb/app/node.service.js.map | 2 +- .../app/node.service.spec.js.map | 2 +- .../app/portfolio/index.js.map | 2 +- .../app/portfolio/portfolio.component.js.map | 2 +- .../portfolio/portfolio.component.spec.js.map | 2 +- .../simmvaluationweb/app/shared/index.js.map | 2 +- .../app/valuations/index.js.map | 2 +- .../valuations/valuations.component.js.map | 2 +- .../valuations.component.spec.js.map | 2 +- .../app/view-trade/index.js.map | 2 +- .../app/view-trade/shared/index.js.map | 2 +- .../app/view-trade/view-trade.component.html | 4 - .../view-trade/view-trade.component.js.map | 2 +- .../view-trade.component.spec.js.map | 2 +- .../app/viewmodel/CommonViewModel.js | 1 - .../app/viewmodel/CommonViewModel.js.map | 2 +- .../app/viewmodel/DealViewModel.js.map | 2 +- .../app/viewmodel/FixedLegViewModel.js | 2 +- .../app/viewmodel/FixedLegViewModel.js.map | 2 +- .../app/viewmodel/FloatingLegViewModel.js | 2 +- .../app/viewmodel/FloatingLegViewModel.js.map | 2 +- .../resources/simmvaluationweb/main.js.map | 2 +- .../simmvaluationweb/system-config.js.map | 2 +- .../vendor/jquery/dist/jquery.min.js | 8 +- .../simmvaluationweb/vendor/moment/moment.js | 293 ++++++++++++++---- .../src/main/web/src/app/app.component.ts | 20 +- .../src/main/web/src/app/model/CommonModel.ts | 1 - .../app/view-trade/view-trade.component.html | 4 - .../web/src/app/viewmodel/CommonViewModel.ts | 1 - .../src/app/viewmodel/FixedLegViewModel.ts | 2 +- .../corda/testing/AlwaysSucceedContract.kt | 10 - .../corda/testing/contracts/DummyContract.kt | 3 +- .../testing/contracts/DummyContractV2.kt | 4 - .../testing/contracts/DummyDealContract.kt | 3 - .../testing/contracts/DummyLinearContract.kt | 2 - 86 files changed, 391 insertions(+), 367 deletions(-) delete mode 100644 test-utils/src/main/kotlin/net/corda/testing/AlwaysSucceedContract.kt diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index f820cdd674..1ac641576f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -296,8 +296,8 @@ interface MoveCommand : CommandData { * Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in * order to settle an obligation contract's state object(s). */ - // TODO: Replace SecureHash here with a general contract constraints object - val contractHash: SecureHash? + // TODO: Replace Class here with a general contract constraints object + val contract: Class? } /** Indicates that this transaction replaces the inputs contract state to another contract state */ @@ -333,15 +333,14 @@ interface Contract { */ @Throws(IllegalArgumentException::class) fun verify(tx: LedgerTransaction) - - /** - * Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of - * the contract's contents). - */ - val legalContractReference: SecureHash } // DOCEND 5 +/** The annotated [Contract] implements the legal prose identified by the given URI. */ +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +annotation class LegalProseReference(val uri: String) + /** * Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract. * @@ -427,7 +426,7 @@ fun JarInputStream.extractFile(path: String, outputTo: OutputStream) { * A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot * use brute force techniques and reveal the content of a Merkle-leaf hashed value. * Because this salt serves the role of the seed to compute nonces, its size and entropy should be equal to the - * underlying hash function used for Merkle tree generation, currently [SHA256], which has an output of 32 bytes. + * underlying hash function used for Merkle tree generation, currently [SecureHash.SHA256], which has an output of 32 bytes. * There are two constructors, one that generates a new 32-bytes random salt, and another that takes a [ByteArray] input. * The latter is required in cases where the salt value needs to be pre-generated (agreed between transacting parties), * but it is highlighted that one should always ensure it has sufficient entropy. diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt index 94cbfc9ca2..8096b63753 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt @@ -1,7 +1,6 @@ package net.corda.core.contracts import net.corda.contracts.asset.Cash -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction import net.corda.finance.DOLLARS @@ -29,7 +28,6 @@ class TransactionEncumbranceTests { val timeLock = DummyTimeLock.State(FIVE_PM) class DummyTimeLock : Contract { - override val legalContractReference = SecureHash.sha256("DummyTimeLock") override fun verify(tx: LedgerTransaction) { val timeLockInput = tx.inputsOfType().singleOrNull() ?: return val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window") diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index cc6e06dba6..e881f3a699 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -3,7 +3,6 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps @@ -212,9 +211,6 @@ class ContractUpgradeFlowTest { override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner)) override fun verify(tx: LedgerTransaction) {} - - // Dummy Cash contract for testing. - override val legalContractReference = SecureHash.sha256("") } @StartableByRPC diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index faf5bc13d9..b5876f52a9 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -17,8 +17,6 @@ class VaultUpdateTests { override fun verify(tx: LedgerTransaction) { } - - override val legalContractReference: SecureHash = SecureHash.sha256("") } private class DummyState : ContractState { diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 7cc959190e..20bf1b8f88 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -1,7 +1,6 @@ package net.corda.core.serialization import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder @@ -20,8 +19,6 @@ val TEST_PROGRAM_ID = TransactionSerializationTests.TestCash() class TransactionSerializationTests : TestDependencyInjectionBase() { class TestCash : Contract { - override val legalContractReference = SecureHash.sha256("TestCash") - override fun verify(tx: LedgerTransaction) { } diff --git a/docs/source/api-contracts.rst b/docs/source/api-contracts.rst index 504c931e06..6da93efeb3 100644 --- a/docs/source/api-contracts.rst +++ b/docs/source/api-contracts.rst @@ -22,9 +22,7 @@ The ``Contract`` interface is defined as follows: Where: - * ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this - contract type are valid -* ``legalContractReference`` is the hash of the legal prose contract that ``verify`` seeks to express in code +* ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this contract type are valid verify() -------- @@ -187,8 +185,6 @@ execution of ``verify()``: } } } - - override val legalContractReference: SecureHash = SecureHash.sha256("X contract hash") } .. sourcecode:: java @@ -209,9 +205,6 @@ execution of ``verify()``: // Transfer verification logic. } } - - private final SecureHash legalContractReference = SecureHash.sha256("X contract hash"); - @Override public final SecureHash getLegalContractReference() { return legalContractReference; } } Grouping states @@ -297,13 +290,5 @@ We can now verify these groups individually: Legal prose ----------- -Current, ``legalContractReference`` is simply the SHA-256 hash of a contract: - -.. container:: codeset - - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - -In the future, a contract's legal prose will be included as an attachment instead. \ No newline at end of file +Currently, a ``Contract`` subtype may refer to the legal prose it implements via a ``LegalProseReference`` annotation. +In the future, a contract's legal prose will be included as an attachment. diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt index efe6d398df..daa6d6e149 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt @@ -2,7 +2,6 @@ package net.corda.docs import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.containsAny import net.corda.core.flows.FinalityFlow @@ -11,8 +10,6 @@ import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub -import net.corda.core.node.services.Vault import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.serialization.CordaSerializable @@ -35,7 +32,7 @@ enum class WorkflowState { * Minimal contract to encode a simple workflow with one initial state and two possible eventual states. * It is assumed one party unilaterally submits and the other manually retrieves the deal and completes it. */ -data class TradeApprovalContract(override val legalContractReference: SecureHash = SecureHash.sha256("Example of workflow type transaction")) : Contract { +data class TradeApprovalContract(private val blank: Void? = null) : Contract { interface Commands : CommandData { class Issue : TypeOnlyCommandData(), Commands // Record receipt of deal details diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 59b3b81da8..c54e5cf6da 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -37,17 +37,11 @@ Just as every Corda state must implement the ``ContractState`` interface, every // Implements the contract constraints in code. @Throws(IllegalArgumentException::class) fun verify(tx: LedgerTransaction) - - // Expresses the contract constraints as legal prose. - val legalContractReference: SecureHash } You can read about function declarations in Kotlin `here `_. -We can see that ``Contract`` expresses its constraints in two ways: - -* In legal prose, through a hash referencing a legal contract that expresses the contract's constraints in legal prose -* In code, through a ``verify`` function that takes a transaction as input, and: +We can see that ``Contract`` expresses its constraints through a ``verify`` function that takes a transaction as input, and: * Throws an ``IllegalArgumentException`` if it rejects the transaction proposal * Returns silently if it accepts the transaction proposal @@ -113,9 +107,6 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) } } - - // The legal contract reference - we'll leave this as a dummy hash for now. - override val legalContractReference = SecureHash.zeroHash } .. code-block:: java @@ -160,10 +151,6 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi return null; }); } - - // The legal contract reference - we'll leave this as a dummy hash for now. - private final SecureHash legalContractReference = SecureHash.Companion.getZeroHash(); - @Override public final SecureHash getLegalContractReference() { return legalContractReference; } } If you're following along in Java, you'll also need to rename ``TemplateContract.java`` to ``IOUContract.java``. diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index 1baaccf4e3..78788090c3 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -152,8 +152,7 @@ The vaults of Node A and Node B should both display the following output: value: 99 lender: "CN=NodeA,O=NodeA,L=London,C=GB" borrower: "CN=NodeB,O=NodeB,L=New York,C=US" - contract: - legalContractReference: "559322B95BCF7913E3113962DC3F3CBD71C818C66977721580C045DC41C813A5" + contract: {} participants: - "CN=NodeA,O=NodeA,L=London,C=GB" - "CN=NodeB,O=NodeB,L=New York,C=US" diff --git a/docs/source/tutorial-contract-clauses.rst b/docs/source/tutorial-contract-clauses.rst index c8b3ca98cd..97ad1ba675 100644 --- a/docs/source/tutorial-contract-clauses.rst +++ b/docs/source/tutorial-contract-clauses.rst @@ -66,8 +66,6 @@ We start by defining the ``CommercialPaper`` class. As in the previous tutorial, .. sourcecode:: kotlin class CommercialPaper : Contract { - override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") - override fun verify(tx: LedgerTransaction) = verifyClause(tx, Clauses.Group(), tx.commands.select()) interface Commands : CommandData { @@ -79,11 +77,6 @@ We start by defining the ``CommercialPaper`` class. As in the previous tutorial, .. sourcecode:: java public class CommercialPaper implements Contract { - @Override - public SecureHash getLegalContractReference() { - return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - } - @Override public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException { ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx)); diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index cdee7aa768..1cffea1982 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -59,8 +59,6 @@ Kotlin syntax works. .. sourcecode:: kotlin class CommercialPaper : Contract { - override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - override fun verify(tx: LedgerTransaction) { TODO() } @@ -69,22 +67,16 @@ Kotlin syntax works. .. sourcecode:: java public class CommercialPaper implements Contract { - @Override - public SecureHash getLegalContractReference() { - return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - } - @Override public void verify(LedgerTransaction tx) { throw new UnsupportedOperationException(); } } -Every contract must have at least a ``getLegalContractReference()`` and a ``verify()`` method. In Kotlin we express -a getter without a setter as an immutable property (val). The *legal contract reference* is supposed to be a hash -of a document that describes the legal contract and may take precedence over the code, in case of a dispute. +Every contract must have at least a ``verify()`` method. -.. note:: The way legal contract prose is bound to a smart contract implementation will change in future. +.. note:: In the future there will be a way to bind legal contract prose to a smart contract implementation, + that may take precedence over the code in case of a dispute. The verify method returns nothing. This is intentional: the function either completes correctly, or throws an exception, in which case the transaction is rejected. diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 7ab46f30c1..e31a4dd5ea 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -450,8 +450,7 @@ We can see a list of the states in our node's vault using ``run vaultAndUpdates` linearId: externalId: null id: "84628565-2688-45ef-bb06-aae70fcf3be7" - contract: - legalContractReference: "4DDE2A47C361106CBAEC06CC40FE418A994822A3C8054851FEECD51207BFAF82" + contract: {} participants: - "CN=NodeB,O=NodeB,L=New York,C=US" - "CN=NodeA,O=NodeA,L=London,C=UK" @@ -485,8 +484,7 @@ abbreviated the output below): linearId: externalId: null id: "84628565-2688-45ef-bb06-aae70fcf3be7" - contract: - legalContractReference: "4DDE2A47C361106CBAEC06CC40FE418A994822A3C8054851FEECD51207BFAF82" + contract: {} participants: - "CN=NodeB,O=NodeB,L=New York,C=US" - "CN=NodeA,O=NodeA,L=London,C=UK" diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt index 0e695837ef..9de4da7de7 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt @@ -3,7 +3,6 @@ package net.corda.contracts.universal import net.corda.contracts.BusinessCalendar import net.corda.contracts.FixOf import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction @@ -316,9 +315,6 @@ class UniversalContract : Contract { else -> throw NotImplementedError("replaceFixing - " + arr.javaClass.name) } - override val legalContractReference: SecureHash - get() = throw UnsupportedOperationException() - fun generateIssue(tx: TransactionBuilder, arrangement: Arrangement, at: PartyAndReference, notary: Party) { check(tx.inputStates().isEmpty()) tx.addOutputState(State(listOf(notary), arrangement)) diff --git a/finance/isolated/src/main/kotlin/net/corda/contracts/isolated/AnotherDummyContract.kt b/finance/isolated/src/main/kotlin/net/corda/contracts/isolated/AnotherDummyContract.kt index 9d615cc02b..af0c746ff5 100644 --- a/finance/isolated/src/main/kotlin/net/corda/contracts/isolated/AnotherDummyContract.kt +++ b/finance/isolated/src/main/kotlin/net/corda/contracts/isolated/AnotherDummyContract.kt @@ -1,7 +1,6 @@ package net.corda.contracts.isolated import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction @@ -25,9 +24,6 @@ class AnotherDummyContract : Contract, DummyContractBackdoor { // Always accepts. } - // The "empty contract" - override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org") - override fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { val state = State(magicNumber) return TransactionBuilder(notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index 88e3de772c..68b780c718 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -3,10 +3,9 @@ package net.corda.contracts; import co.paralleluniverse.fibers.Suspendable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import kotlin.*; -import net.corda.contracts.asset.*; +import kotlin.Unit; +import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; -import net.corda.core.crypto.SecureHash; import net.corda.core.crypto.testing.NullPublicKey; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AnonymousParty; @@ -236,13 +235,6 @@ public class JavaCommercialPaper implements Contract { } } - @NotNull - @Override - public SecureHash getLegalContractReference() { - // TODO: Should return hash of the contract's contents, not its URI - return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - } - public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary, Integer encumbrance) { State state = new State(issuance, issuance.getParty(), faceValue, maturityDate); TransactionState output = new TransactionState<>(state, notary, encumbrance); diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index 49ddf736dd..727a750182 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -3,7 +3,6 @@ package net.corda.contracts import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty @@ -45,9 +44,6 @@ val CP_PROGRAM_ID = CommercialPaper() // TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. class CommercialPaper : Contract { - // TODO: should reference the content of the legal agreement, not its URI - override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") - data class State( val issuance: PartyAndReference, override val owner: AbstractParty, diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index 97482b138b..3416b764ab 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -11,7 +11,6 @@ import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.Emoji import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException @@ -60,20 +59,6 @@ val CASH_PROGRAM_ID = Cash() * vaults can ignore the issuer/depositRefs and just examine the amount fields. */ class Cash : OnLedgerAsset() { - /** - * TODO: - * 1) hash should be of the contents, not the URI - * 2) allow the content to be specified at time of instance creation? - * - * Motivation: it's the difference between a state object referencing a programRef, which references a - * legalContractReference and a state object which directly references both. The latter allows the legal wording - * to evolve without requiring code changes. But creates a risk that users create objects governed by a program - * that is inconsistent with the legal contract. - */ - // DOCSTART 2 - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - - // DOCEND 2 override fun extractCommands(commands: Collection>): List> = commands.select() @@ -129,11 +114,11 @@ class Cash : OnLedgerAsset() { /** * A command stating that money has been moved, optionally to fulfil another contract. * - * @param contractHash the contract this move is for the attention of. Only that contract's verify function + * @param contract the contract this move is for the attention of. Only that contract's verify function * should take the moved states into account when considering whether it is valid. Typically this will be * null. */ - data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands + data class Move(override val contract: Class? = null) : FungibleAsset.Commands.Move, Commands /** * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt index 26a2198adb..5b7bcb65f4 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt @@ -2,13 +2,13 @@ package net.corda.contracts.asset import net.corda.contracts.Commodity import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import java.security.PublicKey import net.corda.finance.utils.sumCommodities import net.corda.finance.utils.sumCommoditiesOrNull import net.corda.finance.utils.sumCommoditiesOrZero @@ -34,18 +34,6 @@ val COMMODITY_PROGRAM_ID = CommodityContract() */ // TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc. class CommodityContract : OnLedgerAsset() { - /** - * TODO: - * 1) hash should be of the contents, not the URI - * 2) allow the content to be specified at time of instance creation? - * - * Motivation: it's the difference between a state object referencing a programRef, which references a - * legalContractReference and a state object which directly references both. The latter allows the legal wording - * to evolve without requiring code changes. But creates a risk that users create objects governed by a program - * that is inconsistent with the legal contract - */ - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html") - /** A state representing a commodity claim against some party */ data class State( override val amount: Amount>, @@ -57,7 +45,7 @@ class CommodityContract : OnLedgerAsset = Collections.singleton(owner.owningKey) override val participants = listOf(owner) override fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset @@ -74,11 +62,11 @@ class CommodityContract : OnLedgerAsset? = null) : FungibleAsset.Commands.Move, Commands /** * Allows new commodity states to be issued into existence: the nonce ("number used once") ensures the transaction diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index de30086bd5..d34377caed 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -75,18 +75,6 @@ val OBLIGATION_PROGRAM_ID = Obligation() * @param P the product the obligation is for payment of. */ class Obligation

: Contract { - /** - * TODO: - * 1) hash should be of the contents, not the URI - * 2) allow the content to be specified at time of instance creation? - * - * Motivation: it's the difference between a state object referencing a programRef, which references a - * legalContractReference and a state object which directly references both. The latter allows the legal wording - * to evolve without requiring code changes. But creates a risk that users create objects governed by a program - * that is inconsistent with the legal contract. - */ - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html") - /** * Represents where in its lifecycle a contract state is, which in turn controls the commands that can be applied * to the state. Most states will not leave the [NORMAL] lifecycle. Note that settled (as an end lifecycle) is @@ -200,11 +188,11 @@ class Obligation

: Contract { /** * A command stating that a debt has been moved, optionally to fulfil another contract. * - * @param contractHash the contract this move is for the attention of. Only that contract's verify function + * @param contract the contract this move is for the attention of. Only that contract's verify function * should take the moved states into account when considering whether it is valid. Typically this will be * null. */ - data class Move(override val contractHash: SecureHash? = null) : Commands, FungibleAsset.Commands.Move + data class Move(override val contract: Class? = null) : Commands, FungibleAsset.Commands.Move /** * Allows new obligation states to be issued into existence: the nonce ("number used once") ensures the @@ -352,15 +340,15 @@ class Obligation

: Contract { // // That would pass this check. Ensuring they do not is best addressed in the transaction generation stage. val assetStates = tx.outputsOfType>() - val acceptableAssetStates = assetStates - // TODO: This filter is nonsense, because it just checks there is an asset contract loaded, we need to - // verify the asset contract is the asset contract we expect. - // Something like: - // attachments.mustHaveOneOf(key.acceptableAssetContract) - .filter { it.contract.legalContractReference in template.acceptableContracts } - // Restrict the states to those of the correct issuance definition (this normally - // covers issued product and obligor, but is opaque to us) - .filter { it.amount.token in template.acceptableIssuedProducts } + val acceptableContract = tx.attachments.any { it.id in template.acceptableContracts } + requireThat { + "an acceptable contract is attached" using acceptableContract + } + val acceptableAssetStates = assetStates.filter { + // Restrict the states to those of the correct issuance definition (this normally + // covers issued product and obligor, but is opaque to us) + it.amount.token in template.acceptableIssuedProducts + } // Catch that there's nothing useful here, so we can dump out a useful error requireThat { "there are fungible asset state outputs" using (assetStates.isNotEmpty()) @@ -387,8 +375,8 @@ class Obligation

: Contract { requireThat { // Insist that we can be the only contract consuming inputs, to ensure no other contract can think it's being // settled as well - "all move commands relate to this contract" using (moveCommands.map { it.value.contractHash } - .all { it == null || it == Obligation

().legalContractReference }) + "all move commands relate to this contract" using (moveCommands.map { it.value.contract } + .all { it == null || it == this@Obligation.javaClass }) // Settle commands exclude all other commands, so we don't need to check for contracts moving at the same // time. "amounts paid must match recipients to settle" using inputs.map { it.owner }.containsAll(amountReceivedByOwner.keys) @@ -486,7 +474,7 @@ class Obligation

: Contract { * Generate a transaction performing close-out netting of two or more states. * * @param signer the party which will sign the transaction. Must be one of the obligor or beneficiary. - * @param states two or more states, which must be compatible for bilateral netting (same issuance definitions, + * @param inputs two or more states, which must be compatible for bilateral netting (same issuance definitions, * and same parties involved). */ fun generateCloseOutNetting(tx: TransactionBuilder, @@ -542,11 +530,12 @@ class Obligation

: Contract { */ fun generateCashIssue(tx: TransactionBuilder, obligor: AbstractParty, + acceptableContract: SecureHash, amount: Amount>, dueBefore: Instant, beneficiary: AbstractParty, notary: Party) { - val issuanceDef = Terms(NonEmptySet.of(Cash().legalContractReference), NonEmptySet.of(amount.token), dueBefore) + val issuanceDef = Terms(NonEmptySet.of(acceptableContract), NonEmptySet.of(amount.token), dueBefore) OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, amount.quantity, beneficiary), notary), Commands.Issue()) } diff --git a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt index e1c9274628..5d93827b6e 100644 --- a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt +++ b/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt @@ -1,7 +1,6 @@ package net.corda.contracts.asset import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty @@ -22,8 +21,6 @@ import java.security.PublicKey import java.util.* class DummyFungibleContract : OnLedgerAsset() { - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - override fun extractCommands(commands: Collection>): List> = commands.select() @@ -82,7 +79,7 @@ class DummyFungibleContract : OnLedgerAsset? = null) : FungibleAsset.Commands.Move, Commands data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index d8163cef45..e285f325f0 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -5,6 +5,7 @@ import net.corda.contracts.NetType import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -28,26 +29,26 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue class ObligationTests { - val defaultRef = OpaqueBytes.of(1) - val defaultIssuer = MEGA_CORP.ref(defaultRef) - val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer - val trustedCashContract = NonEmptySet.of(SecureHash.randomSHA256() as SecureHash) - val megaIssuedDollars = NonEmptySet.of(Issued(defaultIssuer, USD)) - val megaIssuedPounds = NonEmptySet.of(Issued(defaultIssuer, GBP)) - val fivePm: Instant = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS) + 17.hours - val sixPm: Instant = fivePm + 1.hours - val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) - val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) - val inState = Obligation.State( + private val defaultRef = OpaqueBytes.of(1) + private val defaultIssuer = MEGA_CORP.ref(defaultRef) + private val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer + private val trustedCashContract = NonEmptySet.of(SecureHash.randomSHA256() as SecureHash) + private val megaIssuedDollars = NonEmptySet.of(Issued(defaultIssuer, USD)) + private val megaIssuedPounds = NonEmptySet.of(Issued(defaultIssuer, GBP)) + private val fivePm: Instant = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS) + 17.hours + private val sixPm: Instant = fivePm + 1.hours + private val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) + private val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) + private val inState = Obligation.State( lifecycle = Lifecycle.NORMAL, obligor = MEGA_CORP, template = megaCorpDollarSettlement, quantity = 1000.DOLLARS.quantity, beneficiary = CHARLIE ) - val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) - val miniCorpServices = MockServices(MINI_CORP_KEY) - val notaryServices = MockServices(DUMMY_NOTARY_KEY) + private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) + private val miniCorpServices = MockServices(MINI_CORP_KEY) + private val notaryServices = MockServices(DUMMY_NOTARY_KEY) private fun cashObligationTestRoots( group: LedgerDSL @@ -294,7 +295,7 @@ class ObligationTests { // Generate a transaction issuing the obligation. var tx = TransactionBuilder(null).apply { val amount = Amount(100, Issued(defaultIssuer, USD)) - Obligation().generateCashIssue(this, ALICE, amount, dueBefore, + Obligation().generateCashIssue(this, ALICE, cashContractBytes.sha256(), amount, dueBefore, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) } var stx = miniCorpServices.signInitialTransaction(tx) @@ -312,7 +313,7 @@ class ObligationTests { stx.verifyRequiredSignatures() // And set it back - stateAndRef = stx.tx.outRef>(0) + stateAndRef = stx.tx.outRef(0) tx = TransactionBuilder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.NORMAL, DUMMY_NOTARY) } @@ -469,7 +470,8 @@ class ObligationTests { input("Alice's $1,000,000") output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } + attachment(attachment(cashContractBytes.inputStream())) this.verifies() } } @@ -483,7 +485,8 @@ class ObligationTests { output("Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB) } output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } + attachment(attachment(cashContractBytes.inputStream())) this.verifies() } } @@ -496,7 +499,7 @@ class ObligationTests { input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } this `fails with` "all inputs are in the normal state" } } @@ -509,7 +512,8 @@ class ObligationTests { input("Alice's $1,000,000") output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } + attachment(attachment(cashContractBytes.inputStream())) this `fails with` "amount in settle command" } } @@ -517,9 +521,10 @@ class ObligationTests { @Test fun `commodity settlement`() { + val commodityContractBytes = "https://www.big-book-of-banking-law.gov/commodity-claims.html".toByteArray() val defaultFcoj = Issued(defaultIssuer, Commodity.getInstance("FCOJ")!!) val oneUnitFcoj = Amount(1, defaultFcoj) - val obligationDef = Obligation.Terms(NonEmptySet.of(CommodityContract().legalContractReference), NonEmptySet.of(defaultFcoj), TEST_TX_TIME) + val obligationDef = Obligation.Terms(NonEmptySet.of(commodityContractBytes.sha256() as SecureHash), NonEmptySet.of(defaultFcoj), TEST_TX_TIME) val oneUnitFcojObligation = Obligation.State(Obligation.Lifecycle.NORMAL, ALICE, obligationDef, oneUnitFcoj.quantity, NULL_PARTY) // Try settling a simple commodity obligation @@ -533,7 +538,8 @@ class ObligationTests { input("Alice's 1 FCOJ") output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB) } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)) } - command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation::class.java) } + attachment(attachment(commodityContractBytes.inputStream())) verifies() } } @@ -904,8 +910,9 @@ class ObligationTests { assertEquals(expected, actual) } - val Issued.OBLIGATION_DEF: Obligation.Terms - get() = Obligation.Terms(NonEmptySet.of(Cash().legalContractReference), NonEmptySet.of(this), TEST_TX_TIME) - val Amount>.OBLIGATION: Obligation.State + private val cashContractBytes = "https://www.big-book-of-banking-law.gov/cash-claims.html".toByteArray() + private val Issued.OBLIGATION_DEF: Obligation.Terms + get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME) + private val Amount>.OBLIGATION: Obligation.State get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt index 284127a29a..23b8fcb2de 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.declaredField import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* @@ -66,9 +67,6 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { // Always accepts. } - // The "empty contract" - override val legalContractReference: SecureHash = SecureHash.sha256("") - fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { val state = State(magicNumber) return TransactionBuilder(notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) @@ -99,7 +97,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, child) val contract = contractClass.newInstance() as Contract - assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference) + assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.declaredField("legalContractReference").value) } fun fakeAttachment(filepath: String, content: String): ByteArray { @@ -190,7 +188,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl) val contract = contractClass.newInstance() as Contract assertEquals(cl, contract.javaClass.classLoader) - assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference) + assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.declaredField("legalContractReference").value) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 013fbaddc8..ebb97347dd 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -516,8 +516,6 @@ class SerializationOutputTests { override fun verify(tx: LedgerTransaction) { } - - override val legalContractReference: SecureHash = SecureHash.Companion.sha256("FooContractLegal") } class FooState : ContractState { diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index 97d54aa094..c341dfdf53 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -80,8 +80,6 @@ class VaultSchemaTest : TestDependencyInjectionBase() { } private class VaultNoopContract : Contract { - override val legalContractReference = SecureHash.sha256("") - data class VaultNoopState(override val owner: AbstractParty) : OwnableState { override val contract = VaultNoopContract() override val participants: List diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 8c03f061f4..adc945e56b 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -177,9 +177,6 @@ private fun printHelp(parser: OptionParser) { } class AttachmentContract : Contract { - override val legalContractReference: SecureHash - get() = SecureHash.zeroHash // TODO not implemented - override fun verify(tx: LedgerTransaction) { val state = tx.outputsOfType().single() val attachment = tx.attachments.single() diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index 8c032f695d..fc9129fae1 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -3,7 +3,6 @@ package net.corda.irs.contract import com.fasterxml.jackson.annotation.JsonIgnoreProperties import net.corda.contracts.* import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty @@ -193,8 +192,6 @@ class FloatingRatePaymentEvent(date: LocalDate, * This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model. */ class InterestRateSwap : Contract { - override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") - /** * This Common area contains all the information that is not leg specific. */ diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index 6a57156a52..3129a7904f 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -1,9 +1,10 @@ package net.corda.notarydemo.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState import net.corda.contracts.asset.Cash import net.corda.core.contracts.* -import net.corda.core.crypto.sha256 import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AbstractParty @@ -16,7 +17,6 @@ import net.corda.finance.GBP @StartableByRPC class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: Party, private val discriminator: Int) : FlowLogic() { object DoNothingContract : Contract { - override val legalContractReference = byteArrayOf().sha256() override fun verify(tx: LedgerTransaction) {} } diff --git a/samples/simm-valuation-demo/README.md b/samples/simm-valuation-demo/README.md index ef0c08883b..7ea71fe116 100644 --- a/samples/simm-valuation-demo/README.md +++ b/samples/simm-valuation-demo/README.md @@ -19,8 +19,7 @@ This demo was built in partnership with OpenGamma and used their SIMM library. H | Could not find net.corda.(...):(...):0.6-SNAPSHOT | The corda libraries have not been installed into your local maven directory. View the instructions for doing this in the core corda repository | | Execution failed for task ':simm-valuation-demo:buildWeb' : A problem occurred starting process 'command 'ng'' | You need to have `node packet manager` installed in order to build out some of the web resources. This is not a necessary step as we include pre-built web resources but if you do modify the web source, you will need to rebuild this area | - - - - +## Rebuild the web resources +* Get Node.js v6.11.2 which at time of writing is the LTS release +* ../../gradlew installWeb diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 41deacb868..ba1bf69d30 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -1,3 +1,5 @@ +import org.apache.tools.ant.filters.FixCrLfFilter + buildscript { ext.strata_version = '1.1.2' } @@ -117,13 +119,18 @@ task buildWeb(type: Exec, dependsOn: [cleanWeb, npmInstall]) { if (System.getProperty('os.name').toLowerCase().contains('windows')) { commandLine 'cmd', '/c', 'ng', 'build' } else { - commandLine 'ng', 'build' + commandLine 'node_modules/angular-cli/bin/ng', 'build' } } task installWeb(type: Copy, dependsOn: [buildWeb]) { from 'src/main/web/dist' into 'src/main/resources/simmvaluationweb' + ['**/*.js', '**/*.js.map', '**/*.ts'].each { + filesMatching(it) { + filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.LF, fixlast: false) + } + } } publishing { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt index cda90b40d4..fb13e86d76 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt @@ -10,7 +10,6 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.crypto.toBase58String import net.corda.core.messaging.CordaRPCOps -import net.corda.core.node.ServiceHub import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.PortfolioState import net.corda.vega.portfolio.Portfolio @@ -169,7 +168,6 @@ class PortfolioApiUtils(private val ownParty: Party) { ), common = mapOf( "valuationDate" to trade.product.startDate.unadjusted, - "hashLegalDocs" to state.contract.legalContractReference.toString(), "interestRate" to mapOf( "name" to "TODO", "oracle" to "TODO", diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt index 1e7b788be6..420b66dd7b 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt @@ -1,14 +1,13 @@ package net.corda.vega.contracts import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.transactions.LedgerTransaction import java.math.BigDecimal /** * Specifies the contract between two parties that trade an OpenGamma IRS. Currently can only agree to trade. */ -data class OGTrade(override val legalContractReference: SecureHash = SecureHash.sha256("OGTRADE.KT")) : Contract { +class OGTrade : Contract { override fun verify(tx: LedgerTransaction) { requireNotNull(tx.timeWindow) { "must have a time-window" } val groups: List> = tx.groupStates { state -> state.linearId } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt index 38aa7a8acb..e0c3ba4b9a 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt @@ -1,7 +1,6 @@ package net.corda.vega.contracts import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.transactions.LedgerTransaction /** @@ -9,7 +8,7 @@ import net.corda.core.transactions.LedgerTransaction * Implements an agree clause to agree to the portfolio and an update clause to change either the portfolio or valuation * of the portfolio arbitrarily. */ -data class PortfolioSwap(override val legalContractReference: SecureHash = SecureHash.sha256("swordfish")) : Contract { +data class PortfolioSwap(private val blank: Void? = null) : Contract { override fun verify(tx: LedgerTransaction) { requireNotNull(tx.timeWindow) { "must have a time-window)" } val groups: List> = tx.groupStates { state -> state.linearId } diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map index c0424984e9..da51d02cc7 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map @@ -1 +1 @@ -{"version":3,"file":"Deal.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/Deal.ts"],"names":[],"mappings":";AAGA,8BAA8B,uBAAuB,CAAC,CAAA;AACtD,iCAAiC,0BAA0B,CAAC,CAAA;AAC5D,4BAA4B,qBAAqB,CAAC,CAAA;AAIlD,IAAY,CAAC,WAAM,YAAY,CAAC,CAAA;AAEhC,IAAI,gBAAgB,GAAG;IACrB,UAAU,EAAE,gMAAgM;IAC5M,0BAA0B,EAAE,EAE3B;IACD,uBAAuB,EAAE,EAExB;CACF,CAAC;AAEF,IAAI,cAAc,GAAG;IACnB,SAAS,EAAE;QACT,KAAK,EAAE,IAAI,CAAC,IAAI;KACjB;CACF,CAAC;AAEF,IAAI,WAAW,GAAG;IAChB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,SAAS;CACjB,CAAC;AAEF,IAAI,cAAc,GAAG;IACnB,KAAK,EAAE,QAAQ;IACf,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,QAAQ;CAChB,CAAC;AAEF,IAAI,GAAG,GAAG;IACR,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;AACpB,CAAC,CAAA;AAED,wEAAwE;AACxE,IAAI,UAAU,GAAG,UAAC,CAAC,EAAE,CAAC;IACpB,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1B,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAED;IAGE,cAAoB,aAA4B,EAAU,WAAwB,EAAU,UAAsB;QAHpH,iBAwEC;QArEqB,kBAAa,GAAb,aAAa,CAAe;QAAU,gBAAW,GAAX,WAAW,CAAa;QAAU,eAAU,GAAV,UAAU,CAAY;QAFlH,YAAO,GAAG,MAAI,GAAG,EAAE,CAAC,cAAc,EAAE,SAAI,GAAG,EAAE,CAAC,WAAW,EAAE,SAAI,GAAG,EAAE,CAAC,UAAU,EAAE,SAAI,GAAG,EAAE,CAAC,WAAW,EAAE,SAAI,GAAG,EAAE,CAAC,aAAa,EAAE,SAAI,GAAG,EAAE,CAAC,aAAa,EAAE,SAAI,GAAG,EAAE,CAAC,kBAAkB,EAAI,CAAA;QAI1L,oBAAe,GAAG,UAAC,UAA6B,EAAE,QAAyB;YACzE,IAAI,QAAQ,GAAG,IAAI,6BAAa,EAAE,CAAC;YAEnC,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAEjC,QAAQ,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAChD,QAAQ,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;YAChD,QAAQ,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;YACpD,QAAQ,CAAC,SAAS,GAAG,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,GAAG,EAAE,EAAE,CAAC;YAClF,QAAQ,CAAC,gBAAgB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC;YAC9F,QAAQ,CAAC,iBAAiB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC;YAChG,QAAQ,CAAC,eAAe,GAAG,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YAEjE,MAAM,CAAC,QAAQ,CAAC;QAClB,CAAC,CAAC;QAEF,uBAAkB,GAAG,UAAC,aAAmC,EAAE,QAAyB;YAClF,IAAI,WAAW,GAAG,IAAI,mCAAgB,EAAE,CAAC;YAEzC,UAAU,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;YAEvC,WAAW,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YACnD,WAAW,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;YACnD,WAAW,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;YACvD,WAAW,CAAC,gBAAgB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC;YACpG,WAAW,CAAC,iBAAiB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC;YACtG,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YACvD,WAAW,CAAC,cAAc,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YACrE,WAAW,CAAC,eAAe,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YAEtE,MAAM,CAAC,WAAW,CAAC;QACrB,CAAC,CAAC;QAEF,kBAAa,GAAG,UAAC,QAAyB;YACxC,IAAI,MAAM,GAAG,IAAI,yBAAW,EAAE,CAAC;YAE/B,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAE7B,MAAM,CAAC,OAAO,GAAG,KAAI,CAAC,OAAO,CAAC;YAC9B,MAAM,CAAC,gBAAgB,GAAG,QAAQ,CAAC,YAAY,CAAC;YAChD,MAAM,CAAC,kBAAkB,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YACxD,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC/C,MAAM,CAAC,qBAAqB,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC3D,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAE9C,MAAM,CAAC,MAAM,CAAC;QAChB,CAAC,CAAC;QAEF,WAAM,GAAG;YACP,IAAI,QAAQ,GAAG,KAAI,CAAC,aAAa,CAAC,MAAM,CAAC;YACzC,IAAI,aAAa,GAAG,KAAI,CAAC,aAAa,CAAC,WAAW,CAAC;YACnD,IAAI,UAAU,GAAG,KAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;YAE7C,IAAI,QAAQ,GAAG,KAAI,CAAC,eAAe,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YAC1D,IAAI,WAAW,GAAG,KAAI,CAAC,kBAAkB,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;YACnE,IAAI,MAAM,GAAG,KAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC1C,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;YAE7C,IAAI,IAAI,GAAG;gBACT,QAAQ,EAAE,QAAQ;gBAClB,WAAW,EAAE,WAAW;gBACxB,WAAW,EAAE,gBAAgB;gBAC7B,MAAM,EAAE,MAAM;aACf,CAAA;YAED,MAAM,CAAC,IAAI,CAAC;QACd,CAAC,CAAC;IApEmH,CAAC;IAqExH,WAAC;AAAD,CAAC,AAxED,IAwEC;AAxEY,YAAI,OAwEhB,CAAA;AAAA,CAAC","sourcesContent":["import { DealViewModel } from './viewmodel/DealViewModel';\r\nimport { NodeService } from './node.service';\r\nimport { IRSService } from './irs.service';\r\nimport { FixedLegModel } from './model/FixedLegModel';\r\nimport { FloatingLegModel } from './model/FloatingLegModel';\r\nimport { CommonModel } from './model/CommonModel';\r\nimport { FixedLegViewModel } from './viewmodel/FixedLegViewModel';\r\nimport { FloatingLegViewModel } from './viewmodel/FloatingLegViewModel';\r\nimport { CommonViewModel } from './viewmodel/CommonViewModel';\r\nimport * as _ from 'underscore';\r\n\r\nlet calculationModel = {\r\n expression: \"( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))\",\r\n floatingLegPaymentSchedule: {\r\n\r\n },\r\n fixedLegPaymentSchedule: {\r\n\r\n }\r\n};\r\n\r\nlet fixedRateModel = {\r\n ratioUnit: {\r\n value: 0.01 // %\r\n }\r\n};\r\n\r\nlet indexLookup = {\r\n \"GBP\": \"ICE LIBOR\",\r\n \"USD\": \"ICE LIBOR\",\r\n \"EUR\": \"EURIBOR\"\r\n};\r\n\r\nlet calendarLookup = {\r\n \"GBP\": \"London\",\r\n \"USD\": \"NewYork\",\r\n \"EUR\": \"London\"\r\n};\r\n\r\nlet now = () => {\r\n return new Date();\r\n}\r\n\r\n// Copy the value of the field from b to a if it exists on both objects.\r\nlet unionMerge = (a, b) => {\r\n for (let key in b) {\r\n if (a.hasOwnProperty(key)) {\r\n a[key] = b[key];\r\n }\r\n }\r\n}\r\n\r\nexport class Deal {\r\n tradeId = `T${now().getUTCFullYear()}-${now().getUTCMonth()}-${now().getUTCDate()}.${now().getUTCHours()}:${now().getUTCMinutes()}:${now().getUTCSeconds()}:${now().getUTCMilliseconds()}`\r\n\r\n constructor(private dealViewModel: DealViewModel, private nodeService: NodeService, private irsService: IRSService) {}\r\n\r\n toFixedLegModel = (fixedLegVM: FixedLegViewModel, commonVM: CommonViewModel) => {\r\n let fixedLeg = new FixedLegModel();\r\n\r\n unionMerge(fixedLeg, fixedLegVM);\r\n\r\n fixedLeg.notional.token = commonVM.baseCurrency;\r\n fixedLeg.effectiveDate = commonVM.effectiveDate;\r\n fixedLeg.terminationDate = commonVM.terminationDate;\r\n fixedLeg.fixedRate = { ratioUnit: { value: Number(fixedLegVM.fixedRate) / 100 } };\r\n fixedLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).day;\r\n fixedLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).year;\r\n fixedLeg.paymentCalendar = calendarLookup[commonVM.baseCurrency];\r\n\r\n return fixedLeg;\r\n };\r\n\r\n toFloatingLegModel = (floatingLegVM: FloatingLegViewModel, commonVM: CommonViewModel) => {\r\n let floatingLeg = new FloatingLegModel();\r\n\r\n unionMerge(floatingLeg, floatingLegVM);\r\n\r\n floatingLeg.notional.token = commonVM.baseCurrency;\r\n floatingLeg.effectiveDate = commonVM.effectiveDate;\r\n floatingLeg.terminationDate = commonVM.terminationDate;\r\n floatingLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).day;\r\n floatingLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).year;\r\n floatingLeg.index = indexLookup[commonVM.baseCurrency];\r\n floatingLeg.fixingCalendar = [calendarLookup[commonVM.baseCurrency]];\r\n floatingLeg.paymentCalendar = [calendarLookup[commonVM.baseCurrency]];\r\n\r\n return floatingLeg;\r\n };\r\n\r\n toCommonModel = (commonVM: CommonViewModel) => {\r\n let common = new CommonModel();\r\n\r\n unionMerge(common, commonVM);\r\n\r\n common.tradeID = this.tradeId;\r\n common.eligibleCurrency = commonVM.baseCurrency;\r\n common.independentAmounts.token = commonVM.baseCurrency;\r\n common.threshold.token = commonVM.baseCurrency;\r\n common.minimumTransferAmount.token = commonVM.baseCurrency;\r\n common.rounding.token = commonVM.baseCurrency;\r\n\r\n return common;\r\n };\r\n\r\n toJson = () => {\r\n let commonVM = this.dealViewModel.common;\r\n let floatingLegVM = this.dealViewModel.floatingLeg;\r\n let fixedLegVM = this.dealViewModel.fixedLeg;\r\n\r\n let fixedLeg = this.toFixedLegModel(fixedLegVM, commonVM);\r\n let floatingLeg = this.toFloatingLegModel(floatingLegVM, commonVM);\r\n let common = this.toCommonModel(commonVM);\r\n _.assign(fixedLeg.fixedRate, fixedRateModel);\r\n\r\n let json = {\r\n fixedLeg: fixedLeg,\r\n floatingLeg: floatingLeg,\r\n calculation: calculationModel,\r\n common: common\r\n }\r\n\r\n return json;\r\n };\r\n};\r\n"]} \ No newline at end of file +{"version":3,"file":"Deal.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/Deal.ts"],"names":[],"mappings":"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","sourcesContent":["import { DealViewModel } from './viewmodel/DealViewModel';\nimport { NodeService } from './node.service';\nimport { IRSService } from './irs.service';\nimport { FixedLegModel } from './model/FixedLegModel';\nimport { FloatingLegModel } from './model/FloatingLegModel';\nimport { CommonModel } from './model/CommonModel';\nimport { FixedLegViewModel } from './viewmodel/FixedLegViewModel';\nimport { FloatingLegViewModel } from './viewmodel/FloatingLegViewModel';\nimport { CommonViewModel } from './viewmodel/CommonViewModel';\nimport * as _ from 'underscore';\n\nlet calculationModel = {\n expression: \"( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))\",\n floatingLegPaymentSchedule: {\n\n },\n fixedLegPaymentSchedule: {\n\n }\n};\n\nlet fixedRateModel = {\n ratioUnit: {\n value: 0.01 // %\n }\n};\n\nlet indexLookup = {\n \"GBP\": \"ICE LIBOR\",\n \"USD\": \"ICE LIBOR\",\n \"EUR\": \"EURIBOR\"\n};\n\nlet calendarLookup = {\n \"GBP\": \"London\",\n \"USD\": \"NewYork\",\n \"EUR\": \"London\"\n};\n\nlet now = () => {\n return new Date();\n}\n\n// Copy the value of the field from b to a if it exists on both objects.\nlet unionMerge = (a, b) => {\n for (let key in b) {\n if (a.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n}\n\nexport class Deal {\n tradeId = `T${now().getUTCFullYear()}-${now().getUTCMonth()}-${now().getUTCDate()}.${now().getUTCHours()}:${now().getUTCMinutes()}:${now().getUTCSeconds()}:${now().getUTCMilliseconds()}`\n\n constructor(private dealViewModel: DealViewModel, private nodeService: NodeService, private irsService: IRSService) {}\n\n toFixedLegModel = (fixedLegVM: FixedLegViewModel, commonVM: CommonViewModel) => {\n let fixedLeg = new FixedLegModel();\n\n unionMerge(fixedLeg, fixedLegVM);\n\n fixedLeg.notional.token = commonVM.baseCurrency;\n fixedLeg.effectiveDate = commonVM.effectiveDate;\n fixedLeg.terminationDate = commonVM.terminationDate;\n fixedLeg.fixedRate = { ratioUnit: { value: Number(fixedLegVM.fixedRate) / 100 } };\n fixedLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).day;\n fixedLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).year;\n fixedLeg.paymentCalendar = calendarLookup[commonVM.baseCurrency];\n\n return fixedLeg;\n };\n\n toFloatingLegModel = (floatingLegVM: FloatingLegViewModel, commonVM: CommonViewModel) => {\n let floatingLeg = new FloatingLegModel();\n\n unionMerge(floatingLeg, floatingLegVM);\n\n floatingLeg.notional.token = commonVM.baseCurrency;\n floatingLeg.effectiveDate = commonVM.effectiveDate;\n floatingLeg.terminationDate = commonVM.terminationDate;\n floatingLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).day;\n floatingLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).year;\n floatingLeg.index = indexLookup[commonVM.baseCurrency];\n floatingLeg.fixingCalendar = [calendarLookup[commonVM.baseCurrency]];\n floatingLeg.paymentCalendar = [calendarLookup[commonVM.baseCurrency]];\n\n return floatingLeg;\n };\n\n toCommonModel = (commonVM: CommonViewModel) => {\n let common = new CommonModel();\n\n unionMerge(common, commonVM);\n\n common.tradeID = this.tradeId;\n common.eligibleCurrency = commonVM.baseCurrency;\n common.independentAmounts.token = commonVM.baseCurrency;\n common.threshold.token = commonVM.baseCurrency;\n common.minimumTransferAmount.token = commonVM.baseCurrency;\n common.rounding.token = commonVM.baseCurrency;\n\n return common;\n };\n\n toJson = () => {\n let commonVM = this.dealViewModel.common;\n let floatingLegVM = this.dealViewModel.floatingLeg;\n let fixedLegVM = this.dealViewModel.fixedLeg;\n\n let fixedLeg = this.toFixedLegModel(fixedLegVM, commonVM);\n let floatingLeg = this.toFloatingLegModel(floatingLegVM, commonVM);\n let common = this.toCommonModel(commonVM);\n _.assign(fixedLeg.fixedRate, fixedRateModel);\n\n let json = {\n fixedLeg: fixedLeg,\n floatingLeg: floatingLeg,\n calculation: calculationModel,\n common: common\n }\n\n return json;\n };\n};\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js index 53138a1896..228f606b8e 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js @@ -25,8 +25,8 @@ var AppComponent = (function () { this.counterparty = this.httpWrapperService.setCounterparty(value.id); }; AppComponent.prototype.renderX500Name = function (x500Name) { - var name = x500Name - x500Name.split(',').forEach(function(element) { + var name = x500Name; + x500Name.split(',').forEach(function (element) { var keyValue = element.split('='); if (keyValue[0].toUpperCase() == 'CN') { name = keyValue[1]; @@ -38,11 +38,11 @@ var AppComponent = (function () { var _this = this; this.httpWrapperService.getAbsolute("whoami").toPromise().then(function (data) { _this.whoAmI = _this.renderX500Name(data.self.text); - _this.counterParties = data.counterparties.map(function(x) { - return { + _this.counterParties = data.counterparties.map(function (x) { + return { id: x.id, - text: _this.renderX500Name(x.text) - }; + text: this.renderX500Name(x.text) + }; }); if (_this.counterParties.length == 0) { console.log("/whoami is returning no counterparties, the whole app won't run", data); diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map index 176e26eb3e..ab4cbebd9e 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map @@ -1 +1 @@ -{"version":3,"file":"app.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/app.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA6C,eAAe,CAAC,CAAA;AAC7D,uBAAkC,iBAAiB,CAAC,CAAA;AACpD,uBAA+C,iBAAiB,CAAC,CAAA;AACjE,2BAAkC,uBAAuB,CAAC,CAAA;AAE1D,qCAAmC,wBAAwB,CAAC,CAAA;AAgB5D;IAEE,sBAAoB,kBAAsC;QAAtC,uBAAkB,GAAlB,kBAAkB,CAAoB;QAInD,mBAAc,GAAkB,EAAE,CAAC;QAQlC,iBAAY,GAAQ,IAAI,CAAC;IAZ4B,CAAC;IAMvD,+BAAQ,GAAf,UAAgB,KAAU,IAAS,CAAC;;IAE7B,mCAAY,GAAnB,UAAoB,KAAU;QAC5B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAID,+BAAQ,GAAR;QAAA,iBAUC;QATC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YAClE,KAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAC7B,KAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;YAC1C,EAAE,CAAC,CAAC,KAAI,CAAC,cAAc,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpC,OAAO,CAAC,GAAG,CAAC,iEAAiE,EAAE,IAAI,CAAC,CAAC;YACvF,CAAC;QACH,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,0EAA0E,EAAE,KAAK,CAAC,CAAC;QACjG,CAAC,CAAC,CAAC;IACL,CAAC;IAxCH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,UAAU;YACpB,WAAW,EAAE,oBAAoB;YACjC,SAAS,EAAE,CAAC,mBAAmB,EAAE,oDAAoD,CAAC;YACtF,UAAU,EAAE;gBACV,0BAAiB;gBACjB,gBAAO;gBACP,8BAAiB;aAClB;YACD,aAAa,EAAE,wBAAiB,CAAC,IAAI;YACrC,SAAS,EAAE,CAAC,yCAAkB,CAAC,CAAC,wDAAwD;SACzF,CAAC;;oBAAA;IA6BF,mBAAC;AAAD,CAAC,AA3BD,IA2BC;AA3BY,oBAAY,eA2BxB,CAAA","sourcesContent":["import { Component, ViewEncapsulation } from '@angular/core';\r\nimport { ROUTER_DIRECTIVES } from '@angular/router';\r\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\r\nimport { SELECT_DIRECTIVES } from 'ng2-select/ng2-select';\r\nimport * as moment from 'moment';\r\nimport { HttpWrapperService } from './http-wrapper.service';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-root',\r\n templateUrl: 'app.component.html',\r\n styleUrls: ['app.component.css', '../vendor/ng2-select/components/css/ng2-select.css'],\r\n directives: [\r\n ROUTER_DIRECTIVES,\r\n NgClass,\r\n SELECT_DIRECTIVES\r\n ],\r\n encapsulation: ViewEncapsulation.None, // allow external CSS\r\n providers: [HttpWrapperService] // don't declare in children, so that it's a \"singleton\"\r\n})\r\n\r\nexport class AppComponent {\r\n\r\n constructor(private httpWrapperService: HttpWrapperService) {}\r\n\r\n public whoAmI: string; // name\r\n public counterParty: string; // id\r\n public counterParties: Array < any > = [];\r\n\r\n public selected(value: any): void {};\r\n\r\n public refreshValue(value: any): void {\r\n this.counterparty = this.httpWrapperService.setCounterparty(value.id);\r\n }\r\n\r\n private counterparty: any = null;\r\n\r\n ngOnInit() {\r\n this.httpWrapperService.getAbsolute(\"whoami\").toPromise().then((data) => {\r\n this.whoAmI = data.self.text;\r\n this.counterParties = data.counterparties;\r\n if (this.counterParties.length == 0) {\r\n console.log(\"/whoami is returning no counterparties, the whole app won't run\", data);\r\n }\r\n }).catch((error) => {\r\n console.log(\"Error loading who am i (this is really bad, the whole app will not work)\", error);\r\n });\r\n }\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"app.component.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/app.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA6C,eAAe,CAAC,CAAA;AAC7D,uBAAkC,iBAAiB,CAAC,CAAA;AACpD,uBAA+C,iBAAiB,CAAC,CAAA;AACjE,2BAAkC,uBAAuB,CAAC,CAAA;AAE1D,qCAAmC,wBAAwB,CAAC,CAAA;AAgB5D;IAEE,sBAAoB,kBAAsC;QAAtC,uBAAkB,GAAlB,kBAAkB,CAAoB;QAInD,mBAAc,GAAkB,EAAE,CAAC;QAmBlC,iBAAY,GAAQ,IAAI,CAAC;IAvB4B,CAAC;IAMvD,+BAAQ,GAAf,UAAgB,KAAU,IAAS,CAAC;;IAE7B,mCAAY,GAAnB,UAAoB,KAAU;QAC5B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAEM,qCAAc,GAArB,UAAsB,QAAQ;QAC5B,IAAI,IAAI,GAAG,QAAQ,CAAC;QACpB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,OAAO;YACzC,IAAI,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAClC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC;gBACpC,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC;IACd,CAAC;IAID,+BAAQ,GAAR;QAAA,iBAeC;QAdC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YAClE,KAAI,CAAC,MAAM,GAAG,KAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClD,KAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC;gBACrD,MAAM,CAAC;oBACH,EAAE,EAAE,CAAC,CAAC,EAAE;oBACR,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC;iBACpC,CAAC;YACN,CAAC,CAAC,CAAC;YACH,EAAE,CAAC,CAAC,KAAI,CAAC,cAAc,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpC,OAAO,CAAC,GAAG,CAAC,iEAAiE,EAAE,IAAI,CAAC,CAAC;YACvF,CAAC;QACH,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,0EAA0E,EAAE,KAAK,CAAC,CAAC;QACjG,CAAC,CAAC,CAAC;IACL,CAAC;IAxDH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,UAAU;YACpB,WAAW,EAAE,oBAAoB;YACjC,SAAS,EAAE,CAAC,mBAAmB,EAAE,oDAAoD,CAAC;YACtF,UAAU,EAAE;gBACV,0BAAiB;gBACjB,gBAAO;gBACP,8BAAiB;aAClB;YACD,aAAa,EAAE,wBAAiB,CAAC,IAAI;YACrC,SAAS,EAAE,CAAC,yCAAkB,CAAC,CAAC,wDAAwD;SACzF,CAAC;;oBAAA;IA6CF,mBAAC;AAAD,CAAC,AA3CD,IA2CC;AA3CY,oBAAY,eA2CxB,CAAA","sourcesContent":["import { Component, ViewEncapsulation } from '@angular/core';\nimport { ROUTER_DIRECTIVES } from '@angular/router';\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\nimport { SELECT_DIRECTIVES } from 'ng2-select/ng2-select';\nimport * as moment from 'moment';\nimport { HttpWrapperService } from './http-wrapper.service';\n\n@Component({\n moduleId: module.id,\n selector: 'app-root',\n templateUrl: 'app.component.html',\n styleUrls: ['app.component.css', '../vendor/ng2-select/components/css/ng2-select.css'],\n directives: [\n ROUTER_DIRECTIVES,\n NgClass,\n SELECT_DIRECTIVES\n ],\n encapsulation: ViewEncapsulation.None, // allow external CSS\n providers: [HttpWrapperService] // don't declare in children, so that it's a \"singleton\"\n})\n\nexport class AppComponent {\n\n constructor(private httpWrapperService: HttpWrapperService) {}\n\n public whoAmI: string; // name\n public counterParty: string; // id\n public counterParties: Array < any > = [];\n\n public selected(value: any): void {};\n\n public refreshValue(value: any): void {\n this.counterparty = this.httpWrapperService.setCounterparty(value.id);\n }\n\n public renderX500Name(x500Name) {\n var name = x500Name;\n x500Name.split(',').forEach(function (element) {\n var keyValue = element.split('=');\n if (keyValue[0].toUpperCase() == 'CN') {\n name = keyValue[1];\n }\n });\n return name;\n }\n\n private counterparty: any = null;\n\n ngOnInit() {\n this.httpWrapperService.getAbsolute(\"whoami\").toPromise().then((data) => {\n this.whoAmI = this.renderX500Name(data.self.text);\n this.counterParties = data.counterparties.map(function (x) {\n return {\n id: x.id,\n text: this.renderX500Name(x.text)\n };\n });\n if (this.counterParties.length == 0) {\n console.log(\"/whoami is returning no counterparties, the whole app won't run\", data);\n }\n }).catch((error) => {\n console.log(\"Error loading who am i (this is really bad, the whole app will not work)\", error);\n });\n }\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map index 24625e8786..cd3dcb1f5a 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"app.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/app.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,8BAA6B,iBAAiB,CAAC,CAAA;AAE/C,QAAQ,CAAC,WAAW,EAAE;IACpB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,4BAAY,CAAC,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EACxB,gBAAM,CAAC,CAAC,4BAAY,CAAC,EAAE,UAAC,GAAiB;QACvC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC,CAAC;AACR,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { AppComponent } from './app.component';\r\n\r\ndescribe('App: Vega', () => {\r\n beforeEach(() => {\r\n addProviders([AppComponent]);\r\n });\r\n\r\n it('should create the app',\r\n inject([AppComponent], (app: AppComponent) => {\r\n expect(app).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"app.component.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/app.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,8BAA6B,iBAAiB,CAAC,CAAA;AAE/C,QAAQ,CAAC,WAAW,EAAE;IACpB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,4BAAY,CAAC,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EACxB,gBAAM,CAAC,CAAC,4BAAY,CAAC,EAAE,UAAC,GAAiB;QACvC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC,CAAC;AACR,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { AppComponent } from './app.component';\n\ndescribe('App: Vega', () => {\n beforeEach(() => {\n addProviders([AppComponent]);\n });\n\n it('should create the app',\n inject([AppComponent], (app: AppComponent) => {\n expect(app).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map index c33dd41d6d..268eb54ec5 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map @@ -1 +1 @@ -{"version":3,"file":"app.routes.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/app.routes.ts"],"names":[],"mappings":";AAAA,uBAA4C,iBAAiB,CAAC,CAAA;AAC9D,0BAAmC,aAAa,CAAC,CAAA;AACjD,2BAAoC,cAAc,CAAC,CAAA;AACnD,6BAAqC,gBAAgB,CAAC,CAAA;AACtD,2BAAmC,cAAc,CAAC,CAAA;AAElD,IAAM,MAAM,GAAiB;IAC3B,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE;IACzD,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,8BAAkB,EAAE;IACpD,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,gCAAmB,EAAE;IACtD,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,mCAAoB,EAAE;IACzD,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,+BAAkB,EAAE;CAG/D,CAAC;AAEW,0BAAkB,GAAG;IAChC,sBAAa,CAAC,MAAM,CAAC;CACtB,CAAC","sourcesContent":["import { provideRouter, RouterConfig } from '@angular/router';\r\nimport { PortfolioComponent } from './portfolio';\r\nimport { ValuationsComponent } from './valuations';\r\nimport { CreateTradeComponent } from './create-trade';\r\nimport { ViewTradeComponent } from './view-trade';\r\n\r\nconst routes: RouterConfig = [\r\n { path: '', redirectTo: '/portfolio', pathMatch: 'full' },\r\n { path: 'portfolio', component: PortfolioComponent },\r\n { path: 'valuations', component: ValuationsComponent },\r\n { path: 'create-trade', component: CreateTradeComponent },\r\n { path: 'view-trade/:tradeId', component: ViewTradeComponent }\r\n\r\n // { path: '**', component: PageNotFoundComponent }\r\n];\r\n\r\nexport const appRouterProviders = [\r\n provideRouter(routes)\r\n];\r\n"]} \ No newline at end of file +{"version":3,"file":"app.routes.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/app.routes.ts"],"names":[],"mappings":";AAAA,uBAA4C,iBAAiB,CAAC,CAAA;AAC9D,0BAAmC,aAAa,CAAC,CAAA;AACjD,2BAAoC,cAAc,CAAC,CAAA;AACnD,6BAAqC,gBAAgB,CAAC,CAAA;AACtD,2BAAmC,cAAc,CAAC,CAAA;AAElD,IAAM,MAAM,GAAiB;IAC3B,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE;IACzD,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,8BAAkB,EAAE;IACpD,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,gCAAmB,EAAE;IACtD,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,mCAAoB,EAAE;IACzD,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,+BAAkB,EAAE;CAG/D,CAAC;AAEW,0BAAkB,GAAG;IAChC,sBAAa,CAAC,MAAM,CAAC;CACtB,CAAC","sourcesContent":["import { provideRouter, RouterConfig } from '@angular/router';\nimport { PortfolioComponent } from './portfolio';\nimport { ValuationsComponent } from './valuations';\nimport { CreateTradeComponent } from './create-trade';\nimport { ViewTradeComponent } from './view-trade';\n\nconst routes: RouterConfig = [\n { path: '', redirectTo: '/portfolio', pathMatch: 'full' },\n { path: 'portfolio', component: PortfolioComponent },\n { path: 'valuations', component: ValuationsComponent },\n { path: 'create-trade', component: CreateTradeComponent },\n { path: 'view-trade/:tradeId', component: ViewTradeComponent }\n\n // { path: '**', component: PageNotFoundComponent }\n];\n\nexport const appRouterProviders = [\n provideRouter(routes)\n];\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map index 91a048ca80..54e5996fbe 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map @@ -1 +1 @@ -{"version":3,"file":"create-trade.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/create-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,4BAA2B,gBAAgB,CAAC,CAAA;AAC5C,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAyB,iBAAiB,CAAC,CAAA;AAC3C,uBAAuB,iBAAiB,CAAC,CAAA;AACzC,qCAAmC,yBAAyB,CAAC,CAAA;AAE7D;IAAA;QACE,OAAE,GAAW,MAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;QAI1D,eAAU,GAAW,uBAAuB,CAAC;QAG7C,YAAO,GAAW,KAAK,CAAC;QACxB,aAAQ,GAAW,SAAS,CAAC;QAC7B,cAAS,GAAW,OAAO,CAAC;IAC9B,CAAC;IAAD,iBAAC;AAAD,CAAC,AAXD,IAWC;AASD;IAKE,8BACU,UAAsB,EACtB,WAAwB,EACxB,QAAkB,EAClB,MAAc,EACd,kBAAsC;QAVlD,iBAiCC;QA3BW,eAAU,GAAV,UAAU,CAAY;QACtB,gBAAW,GAAX,WAAW,CAAa;QACxB,aAAQ,GAAR,QAAQ,CAAU;QAClB,WAAM,GAAN,MAAM,CAAQ;QACd,uBAAkB,GAAlB,kBAAkB,CAAoB;QAPhD,cAAS,GAAW,EAAE,CAAC;QAoBvB,eAAU,GAAG;YACX,IAAI,IAAI,GAAG,KAAI,CAAC;YAChB,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,QAAQ,EAAE,KAAI,CAAC,IAAI,CAAC;iBAC7D,SAAS,EAAE,CAAC,IAAI,CAAC;gBAChB,KAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAe,KAAI,CAAC,IAAI,CAAC,EAAI,CAAC,CAAC;YAC3D,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;gBACb,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACzB,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;QAnBA,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7E,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,yBAAyB,CAAC;QACjD,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,aAAa,CAAC;IACxC,CAAC;IAED,uCAAQ,GAAR,cAAY,CAAC;IA5Bf;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,kBAAkB;YAC5B,WAAW,EAAE,6BAA6B;YAC1C,SAAS,EAAE,CAAC,sBAAsB,EAAE,4BAA4B,CAAC;YACjE,SAAS,EAAE,CAAC,wBAAU,EAAE,0BAAW,EAAE,iBAAQ,CAAC;SAC/C,CAAC;;4BAAA;IAkCF,2BAAC;AAAD,CAAC,AAjCD,IAiCC;AAjCY,4BAAoB,uBAiChC,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\r\nimport { IRSService } from '../irs.service';\r\nimport { NodeService } from '../node.service';\r\nimport { Location } from '@angular/common';\r\nimport { Router } from '@angular/router';\r\nimport { HttpWrapperService } from '../http-wrapper.service';\r\n\r\nclass DealParams {\r\n id: string = `${100 + Math.floor((Math.random() * 900))}`;\r\n description: string;\r\n counterparty: string;\r\n tradeDate: string;\r\n convention: string = \"USD_FIXED_6M_LIBOR_3M\";\r\n startDate: string;\r\n endDate: string;\r\n buySell: string = \"BUY\";\r\n notional: string = \"1000000\";\r\n fixedRate: string = \"0.015\";\r\n}\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-create-trade',\r\n templateUrl: 'create-trade.component.html',\r\n styleUrls: ['../app.component.css', 'create-trade.component.css'],\r\n providers: [IRSService, NodeService, Location]\r\n})\r\nexport class CreateTradeComponent implements OnInit {\r\n dayCountBasisLookup: string[];\r\n deal: DealParams;\r\n formError: string = \"\";\r\n\r\n constructor(\r\n private irsService: IRSService,\r\n private nodeService: NodeService,\r\n private location: Location,\r\n private router: Router,\r\n private httpWrapperService: HttpWrapperService\r\n ) {\r\n this.dayCountBasisLookup = Object.keys(this.irsService.lookupTable);\r\n this.deal = new DealParams();\r\n this.deal.tradeDate = this.nodeService.formatDateForNode(new Date());\r\n this.deal.startDate = this.nodeService.formatDateForNode(new Date());\r\n this.deal.endDate = this.nodeService.formatDateForNode(new Date(2020, 1, 1));\r\n this.deal.convention = \"EUR_FIXED_1Y_EURIBOR_3M\";\r\n this.deal.description = \"description\";\r\n }\r\n\r\n ngOnInit() {}\r\n\r\n createDeal = () => {\r\n var that = this;\r\n this.httpWrapperService.putWithCounterparty(\"trades\", this.deal)\r\n .toPromise().then(() => {\r\n this.router.navigateByUrl(`/view-trade/${this.deal.id}`);\r\n }).catch((error) => {\r\n that.formError = error;\r\n });\r\n };\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"create-trade.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/create-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,4BAA2B,gBAAgB,CAAC,CAAA;AAC5C,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAyB,iBAAiB,CAAC,CAAA;AAC3C,uBAAuB,iBAAiB,CAAC,CAAA;AACzC,qCAAmC,yBAAyB,CAAC,CAAA;AAE7D;IAAA;QACE,OAAE,GAAW,MAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;QAI1D,eAAU,GAAW,uBAAuB,CAAC;QAG7C,YAAO,GAAW,KAAK,CAAC;QACxB,aAAQ,GAAW,SAAS,CAAC;QAC7B,cAAS,GAAW,OAAO,CAAC;IAC9B,CAAC;IAAD,iBAAC;AAAD,CAAC,AAXD,IAWC;AASD;IAKE,8BACU,UAAsB,EACtB,WAAwB,EACxB,QAAkB,EAClB,MAAc,EACd,kBAAsC;QAVlD,iBAiCC;QA3BW,eAAU,GAAV,UAAU,CAAY;QACtB,gBAAW,GAAX,WAAW,CAAa;QACxB,aAAQ,GAAR,QAAQ,CAAU;QAClB,WAAM,GAAN,MAAM,CAAQ;QACd,uBAAkB,GAAlB,kBAAkB,CAAoB;QAPhD,cAAS,GAAW,EAAE,CAAC;QAoBvB,eAAU,GAAG;YACX,IAAI,IAAI,GAAG,KAAI,CAAC;YAChB,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,QAAQ,EAAE,KAAI,CAAC,IAAI,CAAC;iBAC7D,SAAS,EAAE,CAAC,IAAI,CAAC;gBAChB,KAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAe,KAAI,CAAC,IAAI,CAAC,EAAI,CAAC,CAAC;YAC3D,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;gBACb,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACzB,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;QAnBA,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7E,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,yBAAyB,CAAC;QACjD,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,aAAa,CAAC;IACxC,CAAC;IAED,uCAAQ,GAAR,cAAY,CAAC;IA5Bf;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,kBAAkB;YAC5B,WAAW,EAAE,6BAA6B;YAC1C,SAAS,EAAE,CAAC,sBAAsB,EAAE,4BAA4B,CAAC;YACjE,SAAS,EAAE,CAAC,wBAAU,EAAE,0BAAW,EAAE,iBAAQ,CAAC;SAC/C,CAAC;;4BAAA;IAkCF,2BAAC;AAAD,CAAC,AAjCD,IAiCC;AAjCY,4BAAoB,uBAiChC,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\nimport { IRSService } from '../irs.service';\nimport { NodeService } from '../node.service';\nimport { Location } from '@angular/common';\nimport { Router } from '@angular/router';\nimport { HttpWrapperService } from '../http-wrapper.service';\n\nclass DealParams {\n id: string = `${100 + Math.floor((Math.random() * 900))}`;\n description: string;\n counterparty: string;\n tradeDate: string;\n convention: string = \"USD_FIXED_6M_LIBOR_3M\";\n startDate: string;\n endDate: string;\n buySell: string = \"BUY\";\n notional: string = \"1000000\";\n fixedRate: string = \"0.015\";\n}\n\n@Component({\n moduleId: module.id,\n selector: 'app-create-trade',\n templateUrl: 'create-trade.component.html',\n styleUrls: ['../app.component.css', 'create-trade.component.css'],\n providers: [IRSService, NodeService, Location]\n})\nexport class CreateTradeComponent implements OnInit {\n dayCountBasisLookup: string[];\n deal: DealParams;\n formError: string = \"\";\n\n constructor(\n private irsService: IRSService,\n private nodeService: NodeService,\n private location: Location,\n private router: Router,\n private httpWrapperService: HttpWrapperService\n ) {\n this.dayCountBasisLookup = Object.keys(this.irsService.lookupTable);\n this.deal = new DealParams();\n this.deal.tradeDate = this.nodeService.formatDateForNode(new Date());\n this.deal.startDate = this.nodeService.formatDateForNode(new Date());\n this.deal.endDate = this.nodeService.formatDateForNode(new Date(2020, 1, 1));\n this.deal.convention = \"EUR_FIXED_1Y_EURIBOR_3M\";\n this.deal.description = \"description\";\n }\n\n ngOnInit() {}\n\n createDeal = () => {\n var that = this;\n this.httpWrapperService.putWithCounterparty(\"trades\", this.deal)\n .toPromise().then(() => {\n this.router.navigateByUrl(`/view-trade/${this.deal.id}`);\n }).catch((error) => {\n that.formError = error;\n });\n };\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map index 0b53d9d455..32b7d22eed 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"create-trade.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/create-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,wBAAwB,EAAE;IACjC,EAAE,CAAC,2BAA2B,EAAE;QAC9B,6CAA6C;QAC7C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { CreateTradeComponent } from './create-trade.component';\r\n\r\ndescribe('Component: CreateTrade', () => {\r\n it('should create an instance', () => {\r\n //let component = new CreateTradeComponent();\r\n //expect(component).toBeTruthy();\r\n });\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"create-trade.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/create-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,wBAAwB,EAAE;IACjC,EAAE,CAAC,2BAA2B,EAAE;QAC9B,6CAA6C;QAC7C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { CreateTradeComponent } from './create-trade.component';\n\ndescribe('Component: CreateTrade', () => {\n it('should create an instance', () => {\n //let component = new CreateTradeComponent();\n //expect(component).toBeTruthy();\n });\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map index a01bb2b88c..ecb07b7343 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,0BAA0B,CAAC,EAAA","sourcesContent":["export * from './create-trade.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,0BAA0B,CAAC,EAAA","sourcesContent":["export * from './create-trade.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map index 9a1fb10668..6c8ed6fe10 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map index 3fda85efe4..9c5819dfac 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map @@ -1 +1 @@ -{"version":3,"file":"environment.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/environment.ts"],"names":[],"mappings":";AAAa,mBAAW,GAAG;IACzB,UAAU,EAAE,KAAK;IACjB,OAAO,EAAE,yBAAyB;CACnC,CAAC","sourcesContent":["export const environment = {\r\n production: false,\r\n APIPath: \"/api/simmvaluationdemo/\"\r\n};\r\n"]} \ No newline at end of file +{"version":3,"file":"environment.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/environment.ts"],"names":[],"mappings":";AAAa,mBAAW,GAAG;IACzB,UAAU,EAAE,KAAK;IACjB,OAAO,EAAE,yBAAyB;CACnC,CAAC","sourcesContent":["export const environment = {\n production: false,\n APIPath: \"/api/simmvaluationdemo/\"\n};\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map index 495c039532..12f56275ab 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map @@ -1 +1 @@ -{"version":3,"file":"http-wrapper.service.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/http-wrapper.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable, EventEmitter } from '@angular/core';\r\nimport { Router, NavigationEnd } from '@angular/router';\r\nimport { Http } from '@angular/http';\r\nimport { environment } from './environment';\r\nimport { Observable } from 'rxjs/Rx';\r\n\r\n@Injectable()\r\nexport class HttpWrapperService {\r\n\r\n private counterparty: string;\r\n\r\n constructor(private http: Http, private router: Router) {\r\n // because components listen on newCounterparty,\r\n // they need to know there is a new value when view is switched\r\n router.events.subscribe((event) => {\r\n if (event instanceof NavigationEnd) { //NavigationEnd?\r\n this.emitNewCounterparty();\r\n }\r\n });\r\n }\r\n\r\n //new CP events\r\n\r\n private emitNewCounterparty() {\r\n if (this.counterparty) {\r\n this.newCounterparty.emit({\r\n value: this.counterparty\r\n });\r\n }\r\n }\r\n public newCounterparty: EventEmitter < any > = new EventEmitter();\r\n\r\n // end new CP events\r\n\r\n\r\n // CP getter and setter\r\n\r\n public setCounterparty(cp) {\r\n this.counterparty = cp;\r\n this.emitNewCounterparty();\r\n return cp; //chainable\r\n }\r\n public getCounterparty() {\r\n return this.counterparty;\r\n }\r\n\r\n // end CP getter and setter\r\n\r\n\r\n // HTTP helpers\r\n\r\n private getPath(resource) {\r\n return environment.APIPath + resource;\r\n }\r\n\r\n // end HTTP helpers\r\n\r\n\r\n // HTTP methods\r\n\r\n public getWithCounterparty(resource): any {\r\n return this.http.get(this.getPath(this.counterparty + \"/\" + resource)).map(res => res.json());\r\n }\r\n\r\n public postWithCounterparty(resource, data): any {\r\n return this.http.post(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\r\n }\r\n\r\n public putWithCounterparty(resource, data): any {\r\n return this.http.put(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\r\n }\r\n\r\n public getAbsolute(resource): any {\r\n return this.http.get(this.getPath(resource)).map(res => res.json());\r\n }\r\n\r\n // end HTTP methods\r\n\r\n\r\n\r\n // *****************************************\r\n // Demo magic - delayed data for valuations\r\n // *****************************************\r\n\r\n private subscription;\r\n private step: number = 0;\r\n private updateDelayedData(data) {\r\n if (!data.portfolio) {\r\n return; // data hasn't fully returned yet, don't do anything\r\n }\r\n\r\n var delayedData: any = {};\r\n\r\n if(this.step > 0) {\r\n delayedData.portfolio = data.portfolio;\r\n delayedData.portfolio.agreed = (this.step > 1);\r\n }\r\n\r\n if(this.step > 2) {\r\n delayedData.marketData = data.marketData;\r\n delayedData.marketData.agreed = (this.step > 3);\r\n }\r\n\r\n if(this.step > 4) {\r\n delayedData.sensitivities = data.sensitivities;\r\n delayedData.sensitivities.agreed = (this.step > 5);\r\n }\r\n\r\n if(this.step > 6) {\r\n delayedData.initialMargin = data.initialMargin;\r\n delayedData.initialMargin.agreed = (this.step > 7);\r\n }\r\n\r\n if(this.step > 8) {\r\n delayedData.confirmation = data.confirmation;\r\n delayedData.confirmation.agreed = (this.step > 9);\r\n }\r\n\r\n if(this.step == 10) {\r\n this.subscription.unsubscribe();\r\n }\r\n return delayedData;\r\n }\r\n\r\n public startDelayedTimer() {\r\n this.step = 0;\r\n\r\n // every x second, update data\r\n let timer = Observable.timer(1000, 2000);\r\n this.subscription = timer.subscribe(t => { this.step++; });\r\n }\r\n\r\n public getDelayedData(data): any {\r\n return this.updateDelayedData(data)\r\n }\r\n\r\n // *****************************************\r\n // end demo magic\r\n // *****************************************\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"http-wrapper.service.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/http-wrapper.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable, EventEmitter } from '@angular/core';\nimport { Router, NavigationEnd } from '@angular/router';\nimport { Http } from '@angular/http';\nimport { environment } from './environment';\nimport { Observable } from 'rxjs/Rx';\n\n@Injectable()\nexport class HttpWrapperService {\n\n private counterparty: string;\n\n constructor(private http: Http, private router: Router) {\n // because components listen on newCounterparty,\n // they need to know there is a new value when view is switched\n router.events.subscribe((event) => {\n if (event instanceof NavigationEnd) { //NavigationEnd?\n this.emitNewCounterparty();\n }\n });\n }\n\n //new CP events\n\n private emitNewCounterparty() {\n if (this.counterparty) {\n this.newCounterparty.emit({\n value: this.counterparty\n });\n }\n }\n public newCounterparty: EventEmitter < any > = new EventEmitter();\n\n // end new CP events\n\n\n // CP getter and setter\n\n public setCounterparty(cp) {\n this.counterparty = cp;\n this.emitNewCounterparty();\n return cp; //chainable\n }\n public getCounterparty() {\n return this.counterparty;\n }\n\n // end CP getter and setter\n\n\n // HTTP helpers\n\n private getPath(resource) {\n return environment.APIPath + resource;\n }\n\n // end HTTP helpers\n\n\n // HTTP methods\n\n public getWithCounterparty(resource): any {\n return this.http.get(this.getPath(this.counterparty + \"/\" + resource)).map(res => res.json());\n }\n\n public postWithCounterparty(resource, data): any {\n return this.http.post(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\n }\n\n public putWithCounterparty(resource, data): any {\n return this.http.put(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\n }\n\n public getAbsolute(resource): any {\n return this.http.get(this.getPath(resource)).map(res => res.json());\n }\n\n // end HTTP methods\n\n\n\n // *****************************************\n // Demo magic - delayed data for valuations\n // *****************************************\n\n private subscription;\n private step: number = 0;\n private updateDelayedData(data) {\n if (!data.portfolio) {\n return; // data hasn't fully returned yet, don't do anything\n }\n\n var delayedData: any = {};\n\n if(this.step > 0) {\n delayedData.portfolio = data.portfolio;\n delayedData.portfolio.agreed = (this.step > 1);\n }\n\n if(this.step > 2) {\n delayedData.marketData = data.marketData;\n delayedData.marketData.agreed = (this.step > 3);\n }\n\n if(this.step > 4) {\n delayedData.sensitivities = data.sensitivities;\n delayedData.sensitivities.agreed = (this.step > 5);\n }\n\n if(this.step > 6) {\n delayedData.initialMargin = data.initialMargin;\n delayedData.initialMargin.agreed = (this.step > 7);\n }\n\n if(this.step > 8) {\n delayedData.confirmation = data.confirmation;\n delayedData.confirmation.agreed = (this.step > 9);\n }\n\n if(this.step == 10) {\n this.subscription.unsubscribe();\n }\n return delayedData;\n }\n\n public startDelayedTimer() {\n this.step = 0;\n\n // every x second, update data\n let timer = Observable.timer(1000, 2000);\n this.subscription = timer.subscribe(t => { this.step++; });\n }\n\n public getDelayedData(data): any {\n return this.updateDelayedData(data)\n }\n\n // *****************************************\n // end demo magic\n // *****************************************\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map index 14e1a28e30..532f3fffb7 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"http-wrapper.service.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/http-wrapper.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,qCAAmC,wBAAwB,CAAC,CAAA;AAE5D,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,yCAAkB,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,yCAAkB,CAAC,EACzB,UAAC,OAA2B;QAC1B,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { HttpWrapperService } from './http-wrapper.service';\r\n\r\ndescribe('Service: HttpWrapper', () => {\r\n beforeEach(() => {\r\n addProviders([HttpWrapperService]);\r\n });\r\n\r\n it('should ...',\r\n inject([HttpWrapperService],\r\n (service: HttpWrapperService) => {\r\n expect(service).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"http-wrapper.service.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/http-wrapper.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,qCAAmC,wBAAwB,CAAC,CAAA;AAE5D,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,yCAAkB,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,yCAAkB,CAAC,EACzB,UAAC,OAA2B;QAC1B,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { HttpWrapperService } from './http-wrapper.service';\n\ndescribe('Service: HttpWrapper', () => {\n beforeEach(() => {\n addProviders([HttpWrapperService]);\n });\n\n it('should ...',\n inject([HttpWrapperService],\n (service: HttpWrapperService) => {\n expect(service).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map index e052eb7c5a..16ec595c18 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,eAAe,CAAC,EAAA;AAC9B,iBAAc,iBAAiB,CAAC,EAAA","sourcesContent":["export * from './environment';\r\nexport * from './app.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,eAAe,CAAC,EAAA;AAC9B,iBAAc,iBAAiB,CAAC,EAAA","sourcesContent":["export * from './environment';\nexport * from './app.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map index 813b4bbf55..889e9b5b79 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map @@ -1 +1 @@ -{"version":3,"file":"irs.service.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/irs.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAE3C;IACE,uBAAmB,GAAW,EAAS,IAAY;QAAhC,QAAG,GAAH,GAAG,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IACzD,oBAAC;AAAD,CAAC,AAFD,IAEC;AAFY,qBAAa,gBAEzB,CAAA;AAGD;IAWE;QAXF,iBAiBC;QAhBC,gBAAW,GAAG;YACZ,QAAQ,EAAE,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC;YAC1C,SAAS,EAAE,IAAI,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC;YAC5C,SAAS,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC;YAC/C,eAAe,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACtD,WAAW,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YAClD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACrD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;SACtD,CAAA;QAID,wBAAmB,GAAa,UAAC,SAAiB;YAChD,MAAM,CAAC,KAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC,CAAA;IAJc,CAAC;IAZlB;QAAC,iBAAU,EAAE;;kBAAA;IAkBb,iBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,kBAAU,aAiBtB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\r\n\r\nexport class DayCountBasis {\r\n constructor(public day: string, public year: string) {}\r\n}\r\n\r\n@Injectable()\r\nexport class IRSService {\r\n lookupTable = {\r\n \"30/360\": new DayCountBasis(\"D30\", \"Y360\"),\r\n \"30E/360\": new DayCountBasis(\"D30E\", \"Y360\"),\r\n \"ACT/360\": new DayCountBasis(\"DActual\", \"Y360\"),\r\n \"ACT/365 Fixed\": new DayCountBasis(\"DActual\", \"Y365F\"),\r\n \"ACT/365 L\": new DayCountBasis(\"DActual\", \"Y365L\"),\r\n \"ACT/ACT ISDA\": new DayCountBasis(\"DActual\", \"YISDA\"),\r\n \"ACT/ACT ICMA\": new DayCountBasis(\"DActual\", \"YICMA\")\r\n }\r\n\r\n constructor() {}\r\n\r\n lookupDayCountBasis: Function = (shorthand: string) => {\r\n return this.lookupTable[shorthand];\r\n }\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"irs.service.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/irs.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAE3C;IACE,uBAAmB,GAAW,EAAS,IAAY;QAAhC,QAAG,GAAH,GAAG,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IACzD,oBAAC;AAAD,CAAC,AAFD,IAEC;AAFY,qBAAa,gBAEzB,CAAA;AAGD;IAWE;QAXF,iBAiBC;QAhBC,gBAAW,GAAG;YACZ,QAAQ,EAAE,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC;YAC1C,SAAS,EAAE,IAAI,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC;YAC5C,SAAS,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC;YAC/C,eAAe,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACtD,WAAW,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YAClD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACrD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;SACtD,CAAA;QAID,wBAAmB,GAAa,UAAC,SAAiB;YAChD,MAAM,CAAC,KAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC,CAAA;IAJc,CAAC;IAZlB;QAAC,iBAAU,EAAE;;kBAAA;IAkBb,iBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,kBAAU,aAiBtB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\n\nexport class DayCountBasis {\n constructor(public day: string, public year: string) {}\n}\n\n@Injectable()\nexport class IRSService {\n lookupTable = {\n \"30/360\": new DayCountBasis(\"D30\", \"Y360\"),\n \"30E/360\": new DayCountBasis(\"D30E\", \"Y360\"),\n \"ACT/360\": new DayCountBasis(\"DActual\", \"Y360\"),\n \"ACT/365 Fixed\": new DayCountBasis(\"DActual\", \"Y365F\"),\n \"ACT/365 L\": new DayCountBasis(\"DActual\", \"Y365L\"),\n \"ACT/ACT ISDA\": new DayCountBasis(\"DActual\", \"YISDA\"),\n \"ACT/ACT ICMA\": new DayCountBasis(\"DActual\", \"YICMA\")\n }\n\n constructor() {}\n\n lookupDayCountBasis: Function = (shorthand: string) => {\n return this.lookupTable[shorthand];\n }\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map index 6d6f88cf21..970745e9c4 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"irs.service.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/irs.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,4BAA2B,eAAe,CAAC,CAAA;AAE3C,QAAQ,CAAC,cAAc,EAAE;IACvB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,wBAAU,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,wBAAU,CAAC,EACjB,UAAC,OAAmB;QAClB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { IRSService } from './irs.service';\r\n\r\ndescribe('Service: IRS', () => {\r\n beforeEach(() => {\r\n addProviders([IRSService]);\r\n });\r\n\r\n it('should ...',\r\n inject([IRSService],\r\n (service: IRSService) => {\r\n expect(service).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"irs.service.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/irs.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,4BAA2B,eAAe,CAAC,CAAA;AAE3C,QAAQ,CAAC,cAAc,EAAE;IACvB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,wBAAU,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,wBAAU,CAAC,EACjB,UAAC,OAAmB;QAClB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { IRSService } from './irs.service';\n\ndescribe('Service: IRS', () => {\n beforeEach(() => {\n addProviders([IRSService]);\n });\n\n it('should ...',\n inject([IRSService],\n (service: IRSService) => {\n expect(service).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js index 581b2feed4..88b73e6f81 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js @@ -23,7 +23,6 @@ var CommonModel = (function () { this.exposure = null; this.localBusinessDay = null; this.dailyInterestAmount = null; - this.hashLegalDocs = null; this.tradeID = null; } return CommonModel; diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map index 5e44b6f2f4..d3d3c98ead 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map @@ -1 +1 @@ -{"version":3,"file":"CommonModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/model/CommonModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAW,IAAI,CAAC;QAC5B,0BAAqB,GAAW,IAAI,CAAC;QACrC,uBAAkB,GAAG;YACnB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,cAAS,GAAG;YACV,KAAK,EAAE,EAAE;SACV,CAAC;QACF,0BAAqB,GAAG;YACtB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,kBAAa,GAAW,IAAI,CAAC;QAC7B,qBAAgB,GAAW,IAAI,CAAC;QAChC,mBAAc,GAAW,IAAI,CAAC;QAC9B,iBAAY,GAAW,IAAI,CAAC;QAC5B,wBAAmB,GAAW,IAAI,CAAC;QACnC,aAAQ,GAAW,IAAI,CAAC;QACxB,qBAAgB,GAAW,IAAI,CAAC;QAChC,wBAAmB,GAAW,IAAI,CAAC;QACnC,kBAAa,GAAW,IAAI,CAAC;QAC7B,YAAO,GAAW,IAAI,CAAC;IAEzB,CAAC;IAAD,kBAAC;AAAD,CAAC,AA1BD,IA0BC;AA1BY,mBAAW,cA0BvB,CAAA","sourcesContent":["export class CommonModel {\r\n baseCurrency: string = null;\r\n eligibleCreditSupport: string = null;\r\n independentAmounts = {\r\n token: \"\"\r\n };\r\n threshold = {\r\n token: \"\"\r\n };\r\n minimumTransferAmount = {\r\n token: \"\"\r\n };\r\n rounding = {\r\n token: \"\"\r\n };\r\n valuationDate: string = null;\r\n notificationTime: string = null;\r\n resolutionTime: string = null;\r\n interestRate: Object = null;\r\n addressForTransfers: string = null;\r\n exposure: Object = null;\r\n localBusinessDay: Object = null;\r\n dailyInterestAmount: string = null;\r\n hashLegalDocs: string = null;\r\n tradeID: string = null;\r\n eligibleCurrency: string;\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"CommonModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/model/CommonModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAW,IAAI,CAAC;QAC5B,0BAAqB,GAAW,IAAI,CAAC;QACrC,uBAAkB,GAAG;YACnB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,cAAS,GAAG;YACV,KAAK,EAAE,EAAE;SACV,CAAC;QACF,0BAAqB,GAAG;YACtB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,kBAAa,GAAW,IAAI,CAAC;QAC7B,qBAAgB,GAAW,IAAI,CAAC;QAChC,mBAAc,GAAW,IAAI,CAAC;QAC9B,iBAAY,GAAW,IAAI,CAAC;QAC5B,wBAAmB,GAAW,IAAI,CAAC;QACnC,aAAQ,GAAW,IAAI,CAAC;QACxB,qBAAgB,GAAW,IAAI,CAAC;QAChC,wBAAmB,GAAW,IAAI,CAAC;QACnC,YAAO,GAAW,IAAI,CAAC;IAEzB,CAAC;IAAD,kBAAC;AAAD,CAAC,AAzBD,IAyBC;AAzBY,mBAAW,cAyBvB,CAAA","sourcesContent":["export class CommonModel {\n baseCurrency: string = null;\n eligibleCreditSupport: string = null;\n independentAmounts = {\n token: \"\"\n };\n threshold = {\n token: \"\"\n };\n minimumTransferAmount = {\n token: \"\"\n };\n rounding = {\n token: \"\"\n };\n valuationDate: string = null;\n notificationTime: string = null;\n resolutionTime: string = null;\n interestRate: Object = null;\n addressForTransfers: string = null;\n exposure: Object = null;\n localBusinessDay: Object = null;\n dailyInterestAmount: string = null;\n tradeID: string = null;\n eligibleCurrency: string;\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map index 1883d7e230..521a3d985a 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FixedLegModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/model/FixedLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,mBAAc,GAAW,IAAI,CAAC;QAC9B,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,cAAS,GAAW,IAAI,CAAC;QACzB,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,eAAU,GAAW,IAAI,CAAC;QAC1B,gBAAW,GAAW,IAAI,CAAC;QAC3B,oBAAe,GAAW,IAAI,CAAC;QAC/B,6BAAwB,GAAW,IAAI,CAAC;IAC1C,CAAC;IAAD,oBAAC;AAAD,CAAC,AAhBD,IAgBC;AAhBY,qBAAa,gBAgBzB,CAAA","sourcesContent":["export class FixedLegModel {\r\n fixedRatePayer: string = null;\r\n notional = {\r\n token: \"\"\r\n };\r\n paymentFrequency: string = null;\r\n effectiveDate: string = null;\r\n terminationDate: string = null;\r\n fixedRate: Object = null;\r\n dayCountBasisDay: string = null;\r\n dayCountBasisYear: string = null;\r\n rollConvention: string = null;\r\n dayInMonth: number = null;\r\n paymentRule: string = null;\r\n paymentCalendar: string = null;\r\n interestPeriodAdjustment: string = null;\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FixedLegModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/model/FixedLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,mBAAc,GAAW,IAAI,CAAC;QAC9B,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,cAAS,GAAW,IAAI,CAAC;QACzB,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,eAAU,GAAW,IAAI,CAAC;QAC1B,gBAAW,GAAW,IAAI,CAAC;QAC3B,oBAAe,GAAW,IAAI,CAAC;QAC/B,6BAAwB,GAAW,IAAI,CAAC;IAC1C,CAAC;IAAD,oBAAC;AAAD,CAAC,AAhBD,IAgBC;AAhBY,qBAAa,gBAgBzB,CAAA","sourcesContent":["export class FixedLegModel {\n fixedRatePayer: string = null;\n notional = {\n token: \"\"\n };\n paymentFrequency: string = null;\n effectiveDate: string = null;\n terminationDate: string = null;\n fixedRate: Object = null;\n dayCountBasisDay: string = null;\n dayCountBasisYear: string = null;\n rollConvention: string = null;\n dayInMonth: number = null;\n paymentRule: string = null;\n paymentCalendar: string = null;\n interestPeriodAdjustment: string = null;\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map index f1e5bb537a..e7d7ce0933 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FloatingLegModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/model/FloatingLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,sBAAiB,GAAW,IAAI,CAAC;QACjC,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,yBAAoB,GAAW,IAAI,CAAC;QACpC,eAAU,GAAW,IAAI,CAAC;QAC1B,oBAAe,GAAW,IAAI,CAAC;QAC/B,gBAAW,GAAW,IAAI,CAAC;QAC3B,iBAAY,GAAW,IAAI,CAAC;QAC5B,6BAAwB,GAAW,IAAI,CAAC;QACxC,uBAAkB,GAAW,IAAI,CAAC;QAClC,cAAS,GAAW,IAAI,CAAC;QACzB,sBAAiB,GAAW,IAAI,CAAC;QACjC,gBAAW,GAAW,IAAI,CAAC;QAC3B,UAAK,GAAW,IAAI,CAAC;QACrB,eAAU,GAAG;YACX,IAAI,EAAE,EAAE;SACT,CAAC;QACF,mBAAc,GAAa,EAAE,CAAC;QAC9B,oBAAe,GAAa,EAAE,CAAC;IACjC,CAAC;IAAD,uBAAC;AAAD,CAAC,AA3BD,IA2BC;AA3BY,wBAAgB,mBA2B5B,CAAA","sourcesContent":["export class FloatingLegModel {\r\n floatingRatePayer: string = null;\r\n notional = {\r\n token: \"\"\r\n };\r\n paymentFrequency: string = null;\r\n effectiveDate: string = null;\r\n terminationDate: string = null;\r\n dayCountBasisDay: string = null;\r\n dayCountBasisYear: string = null;\r\n rollConvention: string = null;\r\n fixingRollConvention: string = null;\r\n dayInMonth: string = null;\r\n resetDayInMonth: string = null;\r\n paymentRule: string = null;\r\n paymentDelay: string = null;\r\n interestPeriodAdjustment: string = null;\r\n fixingPeriodOffset: string = null;\r\n resetRule: string = null;\r\n fixingsPerPayment: string = null;\r\n indexSource: string = null;\r\n index: string = null;\r\n indexTenor = {\r\n name: \"\"\r\n };\r\n fixingCalendar: string[] = [];\r\n paymentCalendar: string[] = [];\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FloatingLegModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/model/FloatingLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,sBAAiB,GAAW,IAAI,CAAC;QACjC,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,yBAAoB,GAAW,IAAI,CAAC;QACpC,eAAU,GAAW,IAAI,CAAC;QAC1B,oBAAe,GAAW,IAAI,CAAC;QAC/B,gBAAW,GAAW,IAAI,CAAC;QAC3B,iBAAY,GAAW,IAAI,CAAC;QAC5B,6BAAwB,GAAW,IAAI,CAAC;QACxC,uBAAkB,GAAW,IAAI,CAAC;QAClC,cAAS,GAAW,IAAI,CAAC;QACzB,sBAAiB,GAAW,IAAI,CAAC;QACjC,gBAAW,GAAW,IAAI,CAAC;QAC3B,UAAK,GAAW,IAAI,CAAC;QACrB,eAAU,GAAG;YACX,IAAI,EAAE,EAAE;SACT,CAAC;QACF,mBAAc,GAAa,EAAE,CAAC;QAC9B,oBAAe,GAAa,EAAE,CAAC;IACjC,CAAC;IAAD,uBAAC;AAAD,CAAC,AA3BD,IA2BC;AA3BY,wBAAgB,mBA2B5B,CAAA","sourcesContent":["export class FloatingLegModel {\n floatingRatePayer: string = null;\n notional = {\n token: \"\"\n };\n paymentFrequency: string = null;\n effectiveDate: string = null;\n terminationDate: string = null;\n dayCountBasisDay: string = null;\n dayCountBasisYear: string = null;\n rollConvention: string = null;\n fixingRollConvention: string = null;\n dayInMonth: string = null;\n resetDayInMonth: string = null;\n paymentRule: string = null;\n paymentDelay: string = null;\n interestPeriodAdjustment: string = null;\n fixingPeriodOffset: string = null;\n resetRule: string = null;\n fixingsPerPayment: string = null;\n indexSource: string = null;\n index: string = null;\n indexTenor = {\n name: \"\"\n };\n fixingCalendar: string[] = [];\n paymentCalendar: string[] = [];\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map index f2667d770b..08209cd8f0 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map @@ -1 +1 @@ -{"version":3,"file":"node.service.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/node.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAG3C,IAAI,UAAU,GAAG,EAAE,CAAC;AAEpB,IAAI,IAAI,GAAG,UAAC,IAAI,EAAE,OAAO;IACvB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAC,GAAG;QACtB,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC;IACb,CAAC,EAAE,UAAC,GAAG;QACL,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AACF,qCAAmC,wBAAwB,CAAC,CAAA;AAG5D;IACE,qBAAoB,kBAAsC;QAD5D,iBAmBC;QAlBqB,uBAAkB,GAAlB,kBAAkB,CAAoB;QAE1D,sBAAiB,GAAa,UAAC,IAAI;YACjC,iEAAiE;YACjE,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C,IAAI,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACpD,MAAM,CAAI,IAAI,CAAC,WAAW,EAAE,SAAI,KAAK,SAAI,GAAK,CAAC;QACjD,CAAC,CAAC;QAEF,YAAO,GAAa,UAAC,MAAM;YACzB,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC,SAAS,EAAE,CAAC;iBACtG,IAAI,CAAC,UAAC,IAAI;gBACT,kDAAkD;gBAClD,IAAI,IAAI,GAAG,IAAI,CAAC;gBAChB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7F,MAAM,CAAC,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;IAjB2D,CAAC;IAFhE;QAAC,iBAAU,EAAE;;mBAAA;IAoBb,kBAAC;AAAD,CAAC,AAnBD,IAmBC;AAnBY,mBAAW,cAmBvB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\r\nimport { Deal } from './Deal'\r\nimport { Observable } from 'rxjs/Rx';\r\nlet curLoading = {};\r\n\r\nlet load = (type, promise) => {\r\n curLoading[type] = true;\r\n return promise.then((arg) => {\r\n curLoading[type] = false;\r\n return arg;\r\n }, (arg) => {\r\n curLoading[type] = false;\r\n throw arg;\r\n });\r\n};\r\nimport { HttpWrapperService } from './http-wrapper.service';\r\n\r\n@Injectable()\r\nexport class NodeService {\r\n constructor(private httpWrapperService: HttpWrapperService) {}\r\n\r\n formatDateForNode: Function = (date) => {\r\n // Produces yyyy-dd-mm. JS is missing proper date formatting libs\r\n let day = (\"0\" + (date.getDate())).slice(-2);\r\n let month = (\"0\" + (date.getMonth() + 1)).slice(-2);\r\n return `${date.getFullYear()}-${month}-${day}`;\r\n };\r\n\r\n getDeal: Function = (dealId) => {\r\n return load('deal' + dealId, this.httpWrapperService.getWithCounterparty('trades/' + dealId).toPromise())\r\n .then((resp) => {\r\n // Do some data modification to simplify the model\r\n let deal = resp;\r\n deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.value * 100).toString().slice(0, 6);\r\n return deal;\r\n });\r\n };\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"node.service.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/node.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAG3C,IAAI,UAAU,GAAG,EAAE,CAAC;AAEpB,IAAI,IAAI,GAAG,UAAC,IAAI,EAAE,OAAO;IACvB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAC,GAAG;QACtB,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC;IACb,CAAC,EAAE,UAAC,GAAG;QACL,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AACF,qCAAmC,wBAAwB,CAAC,CAAA;AAG5D;IACE,qBAAoB,kBAAsC;QAD5D,iBAmBC;QAlBqB,uBAAkB,GAAlB,kBAAkB,CAAoB;QAE1D,sBAAiB,GAAa,UAAC,IAAI;YACjC,iEAAiE;YACjE,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C,IAAI,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACpD,MAAM,CAAI,IAAI,CAAC,WAAW,EAAE,SAAI,KAAK,SAAI,GAAK,CAAC;QACjD,CAAC,CAAC;QAEF,YAAO,GAAa,UAAC,MAAM;YACzB,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC,SAAS,EAAE,CAAC;iBACtG,IAAI,CAAC,UAAC,IAAI;gBACT,kDAAkD;gBAClD,IAAI,IAAI,GAAG,IAAI,CAAC;gBAChB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7F,MAAM,CAAC,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;IAjB2D,CAAC;IAFhE;QAAC,iBAAU,EAAE;;mBAAA;IAoBb,kBAAC;AAAD,CAAC,AAnBD,IAmBC;AAnBY,mBAAW,cAmBvB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\nimport { Deal } from './Deal'\nimport { Observable } from 'rxjs/Rx';\nlet curLoading = {};\n\nlet load = (type, promise) => {\n curLoading[type] = true;\n return promise.then((arg) => {\n curLoading[type] = false;\n return arg;\n }, (arg) => {\n curLoading[type] = false;\n throw arg;\n });\n};\nimport { HttpWrapperService } from './http-wrapper.service';\n\n@Injectable()\nexport class NodeService {\n constructor(private httpWrapperService: HttpWrapperService) {}\n\n formatDateForNode: Function = (date) => {\n // Produces yyyy-dd-mm. JS is missing proper date formatting libs\n let day = (\"0\" + (date.getDate())).slice(-2);\n let month = (\"0\" + (date.getMonth() + 1)).slice(-2);\n return `${date.getFullYear()}-${month}-${day}`;\n };\n\n getDeal: Function = (dealId) => {\n return load('deal' + dealId, this.httpWrapperService.getWithCounterparty('trades/' + dealId).toPromise())\n .then((resp) => {\n // Do some data modification to simplify the model\n let deal = resp;\n deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.value * 100).toString().slice(0, 6);\n return deal;\n });\n };\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map index d51fd3a303..d667a7d3d5 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"node.service.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/node.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,6BAA4B,gBAAgB,CAAC,CAAA;AAE7C,QAAQ,CAAC,eAAe,EAAE;IACxB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,0BAAW,CAAC,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,0BAAW,CAAC,EAClB,UAAC,OAAoB;QACnB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { NodeService } from './node.service';\r\n\r\ndescribe('Service: Node', () => {\r\n beforeEach(() => {\r\n addProviders([NodeService]);\r\n });\r\n\r\n it('should ...',\r\n inject([NodeService],\r\n (service: NodeService) => {\r\n expect(service).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"node.service.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/node.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,6BAA4B,gBAAgB,CAAC,CAAA;AAE7C,QAAQ,CAAC,eAAe,EAAE;IACxB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,0BAAW,CAAC,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,0BAAW,CAAC,EAClB,UAAC,OAAoB;QACnB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { NodeService } from './node.service';\n\ndescribe('Service: Node', () => {\n beforeEach(() => {\n addProviders([NodeService]);\n });\n\n it('should ...',\n inject([NodeService],\n (service: NodeService) => {\n expect(service).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map index f2b9af6d45..dfe8afcd6b 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/portfolio/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,uBAAuB,CAAC,EAAA","sourcesContent":["export * from './portfolio.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/portfolio/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,uBAAuB,CAAC,EAAA","sourcesContent":["export * from './portfolio.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map index 3c2ce20e14..28946cbaab 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map @@ -1 +1 @@ -{"version":3,"file":"portfolio.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/portfolio/portfolio.component.ts"],"names":[],"mappings":"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","sourcesContent":["// not really the Angular way:\r\n/* beautify preserve:start */\r\ndeclare var $;\r\ndeclare var Highcharts;\r\n/* beautify preserve:end */\r\n\r\nimport { Component, OnInit } from '@angular/core';\r\nimport { TAB_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\r\nimport { POPOVER_DIRECTIVES } from 'ng2-popover';\r\nimport { FORM_DIRECTIVES } from '@angular/forms/src/directives' // https://github.com/valor-software/ng2-bootstrap/issues/782\r\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\r\nimport { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\r\nimport { NG_TABLE_DIRECTIVES } from 'ng2-table/ng2-table';\r\nimport { HttpWrapperService } from '../http-wrapper.service';\r\nimport { Router } from '@angular/router';\r\nimport { Observable } from 'rxjs/Rx';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-portfolio',\r\n templateUrl: 'portfolio.component.html',\r\n styleUrls: [\r\n 'portfolio.component.css'\r\n ],\r\n directives: [POPOVER_DIRECTIVES, TAB_DIRECTIVES, NG_TABLE_DIRECTIVES, PAGINATION_DIRECTIVES, NgIf, CORE_DIRECTIVES, FORM_DIRECTIVES]\r\n})\r\n\r\nexport class PortfolioComponent implements OnInit {\r\n\r\n private IDFormatter(id) {\r\n return \"\" + id + \"\";\r\n }\r\n\r\n private defaultFormatter(value) {\r\n return value;\r\n }\r\n\r\n private numberFormatter(n) {\r\n if (!n) {\r\n return \"\";\r\n }\r\n\r\n var a = \"\" + n;\r\n a = a.replace(new RegExp(\"^(\\\\d{\" + (a.length % 3 ? a.length % 3 : 0) + \"})(\\\\d{3})\", \"g\"), \"$1 $2\").replace(/(\\d{3})+?/gi, \"$1 \").trim();\r\n var sep = \",\";\r\n a = a.replace(/\\s/g, sep);\r\n\r\n return a;\r\n }\r\n\r\n public rows: Array < any > = [];\r\n public columns: Array < any > = [\r\n // omitting the sort column on each column would result in no default sorting!\r\n { title: 'ID', name: 'id', sort: '', formatter: this.IDFormatter },\r\n { title: 'Product', name: 'product', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Type', name: 'buySell', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Trade Date', name: 'tradeDate', sort: 'desc', formatter: this.defaultFormatter },\r\n { title: 'Effective Date', name: 'effectiveDate', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Maturity Date', name: 'maturityDate', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Currency', name: 'currency', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Notional', name: 'notional', sort: '', formatter: this.numberFormatter },\r\n { title: 'IM Contribution', name: 'im', sort: '', formatter: this.numberFormatter },\r\n { title: 'PV', name: 'mtm', sort: '', formatter: this.numberFormatter },\r\n { title: 'Included in summary', name: 'marginedText', sort: '', formatter: this.defaultFormatter }\r\n ];\r\n public page: number = 1;\r\n public itemsPerPage: number = 10;\r\n public maxSize: number = 5;\r\n public numPages: number = 1;\r\n public length: number = 0;\r\n\r\n public config: any = {\r\n paging: true,\r\n sorting: { columns: this.columns }\r\n };\r\n\r\n private businessDate: string;\r\n\r\n private data: Array < any > = [];\r\n\r\n private summaryTable: any = {\r\n product: \"Vanilla IRS\",\r\n currency: \"EUR\",\r\n trades: 0,\r\n notional: 0,\r\n im: 0,\r\n mtm: 0\r\n };\r\n\r\n private createTradesChart(TData) {\r\n var TFormat = 'Date: {point.x:%Y-%m-%d}
' + 'IM: {point.y:,.0f}€';\r\n\r\n $('#tradesChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n chart: {\r\n type: 'scatter',\r\n zoomType: 'xy'\r\n },\r\n rangeSelector: {\r\n selected: 4\r\n },\r\n title: {\r\n text: 'Individual Trades'\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'IM'\r\n }\r\n },\r\n series: [{\r\n name: 'Trade',\r\n data: TData,\r\n tooltip: {\r\n pointFormat: TFormat\r\n }\r\n }]\r\n });\r\n }\r\n\r\n private createIMOverVMChart(IMVMData) {\r\n // note there's no \"highstocks\"\r\n $('#IMOverVMChart').highcharts({\r\n credits: {\r\n enabled: false\r\n },\r\n chart: {\r\n type: 'scatter',\r\n zoomType: 'xy'\r\n },\r\n title: {\r\n text: 'Imminent IM over Variation Margin of trades'\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n subtitle: {\r\n text: ''\r\n },\r\n xAxis: {\r\n title: {\r\n enabled: true,\r\n text: 'MTM'\r\n },\r\n startOnTick: true,\r\n endOnTick: true,\r\n showLastLabel: true\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'IM'\r\n }\r\n },\r\n plotOptions: {\r\n scatter: {\r\n marker: {\r\n radius: 5,\r\n states: {\r\n hover: {\r\n enabled: true,\r\n lineColor: 'rgb(100,100,100)'\r\n }\r\n }\r\n },\r\n states: {\r\n hover: {\r\n marker: {\r\n enabled: false\r\n }\r\n }\r\n },\r\n tooltip: {\r\n headerFormat: '{series.name}
',\r\n pointFormat: 'IM: {point.x:,.0f}€
MTM: {point.x:,.0f}€
'\r\n }\r\n }\r\n },\r\n series: [{\r\n name: 'Trade',\r\n data: IMVMData\r\n }]\r\n\r\n });\r\n }\r\n\r\n private createIMVMHistoryChart(IMData, MTMData) {\r\n $('#IMVMHistoryChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n rangeSelector: {\r\n selected: 4\r\n },\r\n title: {\r\n text: 'Portfolio History'\r\n },\r\n subtitle: {\r\n text: 'Initial and Variation Margin Requirements'\r\n },\r\n xAxis: {\r\n type: 'datetime',\r\n dateTimeLabelFormats: { // don't display the dummy year\r\n //day: '%d'\r\n month: '%e. %b',\r\n year: '%b'\r\n },\r\n title: {\r\n text: 'Date'\r\n }\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'Exposure (€)'\r\n },\r\n min: 0\r\n },\r\n plotOptions: {\r\n spline: {\r\n marker: {\r\n enabled: true\r\n }\r\n }\r\n },\r\n series: [{\r\n name: 'Initial Margin',\r\n data: IMData,\r\n type: 'column'\r\n }, {\r\n name: 'Mark to Market',\r\n data: MTMData,\r\n type: 'spline'\r\n }]\r\n });\r\n }\r\n\r\n private createActiveTradesChart(ATData) {\r\n var ATformat = 'Active trades: {point.y:,.0f}
' +\r\n 'IM: {point.x:,.0f}
';\r\n\r\n $('#activeTradesChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n rangeSelector: {\r\n selected: 4\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n xAxis: {\r\n type: 'datetime',\r\n dateTimeLabelFormats: { // don't display the dummy year\r\n //day: '%d'\r\n month: '%e. %b',\r\n year: '%b'\r\n },\r\n title: {\r\n text: 'Date'\r\n }\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'Quantity'\r\n }\r\n },\r\n title: {\r\n text: 'Active Trades'\r\n },\r\n series: [{\r\n name: 'Active trades',\r\n data: ATData,\r\n tooltip: {\r\n pointFormat: ATformat\r\n }\r\n }]\r\n });\r\n }\r\n\r\n private createIMVMHistorySummaryChart(IMData, MTMData) {\r\n $('#IMVMHistorySummaryChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n rangeSelector: {\r\n enabled: false\r\n },\r\n navigator: {\r\n enabled: false\r\n },\r\n scrollbar: {\r\n enabled: false\r\n },\r\n title: {\r\n text: 'Portfolio History'\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n xAxis: {\r\n type: 'datetime',\r\n title: {\r\n text: 'Date'\r\n }\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'Exposure (€)'\r\n },\r\n min: 0\r\n },\r\n plotOptions: {\r\n spline: {\r\n marker: {\r\n enabled: true\r\n }\r\n }\r\n },\r\n dataGrouping: {\r\n approximation: \"average\",\r\n enabled: true,\r\n forced: true,\r\n units: [\r\n ['month', [1]]\r\n ]\r\n },\r\n series: [{\r\n name: 'Initial Margin',\r\n data: IMData,\r\n type: 'column'\r\n }, {\r\n name: 'Mark to Market',\r\n data: MTMData,\r\n type: 'spline'\r\n }]\r\n });\r\n }\r\n\r\n constructor(private httpWrapperService: HttpWrapperService, private router: Router) {}\r\n\r\n private getData() {\r\n if (this.httpWrapperService.getCounterparty()) {\r\n // re-initialize addittive table sums\r\n this.summaryTable.trades = 0;\r\n this.summaryTable.notional = 0;\r\n this.summaryTable.im = 0;\r\n this.summaryTable.mtm = 0;\r\n\r\n this.data = null; //don't leave old data in case of errors\r\n\r\n //trades\r\n this.httpWrapperService.getWithCounterparty(\"trades\").toPromise().then((data) => {\r\n\r\n // trades over time scatter\r\n var TData = [];\r\n\r\n // trades IM over VM scatter\r\n var IMVMData = [];\r\n\r\n $.each(data, (index, value) => {\r\n if (value.margined) {\r\n TData.push([new Date(value.tradeDate).getTime(), value.im]);\r\n\r\n IMVMData.push([value.im, value.mtm]);\r\n }\r\n });\r\n\r\n this.createTradesChart(TData);\r\n\r\n this.createIMOverVMChart(IMVMData);\r\n\r\n // trades table\r\n this.data = data;\r\n this.length = this.data.length;\r\n this.onChangeTable(this.config);\r\n\r\n }).catch((error) => {\r\n console.log(\"Error loading trades\", error);\r\n });\r\n\r\n this.populateSummary().then(() => {\r\n // portfolio history and active trades charts\r\n this.httpWrapperService.getWithCounterparty(\"portfolio/history/aggregated\").toPromise().then((data) => {\r\n // summary table\r\n let lastDay = data\r\n this.summaryTable.trades = lastDay.activeTrades;\r\n this.summaryTable.notional = lastDay.notional;\r\n this.summaryTable.im = lastDay.im;\r\n this.summaryTable.mtm = lastDay.mtm;\r\n\r\n var IMData = [];\r\n var MTMData = [];\r\n var ATData = [];\r\n\r\n $.each(data, (index, value) => {\r\n // new Date(value.date).getTime() when dates are switched to YYYY-MM-DD\r\n IMData.push([value.date, value.im]);\r\n MTMData.push([value.date, value.mtm]);\r\n ATData.push([value.date, value.activeTrades]);\r\n });\r\n\r\n this.createIMVMHistoryChart(IMData, MTMData);\r\n this.createActiveTradesChart(ATData);\r\n\r\n this.createIMVMHistorySummaryChart(IMData, MTMData);\r\n }).catch((error) => {\r\n console.log(\"Error loading portfolio history\", error);\r\n })\r\n })\r\n }\r\n }\r\n\r\n private populateSummary() {\r\n return this.httpWrapperService.getWithCounterparty(\"portfolio/summary\").toPromise().then((data) => {\r\n this.summaryTable.trades = data.trades;\r\n this.summaryTable.notional = data.notional;\r\n }).catch((error) => {\r\n console.log(\"Error loading portfolio summary\", error);\r\n })\r\n }\r\n\r\n counterpartySubscription: any;\r\n ngOnInit() {\r\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\r\n this.businessDate = data.businessDate;\r\n }).catch((error) => {\r\n console.log(\"Error loading business date\", error);\r\n });\r\n\r\n Highcharts.setOptions({\r\n lang: {\r\n thousandsSep: ','\r\n }\r\n });\r\n\r\n this.getData();\r\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\r\n this.getData();\r\n });\r\n }\r\n\r\n ngOnDestroy() {\r\n this.counterpartySubscription.unsubscribe();\r\n }\r\n\r\n // table helper functions\r\n\r\n public changePage(page: any, data: Array < any > = this.data): Array < any > {\r\n let start = (page.page - 1) * page.itemsPerPage;\r\n let end = page.itemsPerPage > -1 ? (start + page.itemsPerPage) : data.length;\r\n return data.slice(start, end);\r\n }\r\n\r\n public changeSort(data: any, config: any): any {\r\n if (!config.sorting) {\r\n return data;\r\n }\r\n\r\n let columns = this.config.sorting.columns || [];\r\n let columnName: string = void 0;\r\n let sort: string = void 0;\r\n\r\n for (let i = 0; i < columns.length; i++) {\r\n if (columns[i].sort !== '') {\r\n columnName = columns[i].name;\r\n sort = columns[i].sort;\r\n }\r\n }\r\n\r\n if (!columnName) {\r\n return data;\r\n }\r\n\r\n // simple sorting\r\n return data.sort((previous: any, current: any) => {\r\n if (previous[columnName] > current[columnName]) {\r\n return sort === 'desc' ? -1 : 1;\r\n } else if (previous[columnName] < current[columnName]) {\r\n return sort === 'asc' ? -1 : 1;\r\n }\r\n return 0;\r\n });\r\n }\r\n\r\n public onChangeTable(config: any, page: any = { page: this.page, itemsPerPage: this.itemsPerPage }): any {\r\n if (config.sorting) {\r\n Object.assign(this.config.sorting, config.sorting);\r\n }\r\n\r\n let sortedData = this.changeSort(this.data, this.config);\r\n this.rows = page && config.paging ? this.changePage(page, sortedData) : sortedData;\r\n this.length = sortedData.length;\r\n }\r\n\r\n // end table helper functions\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"portfolio.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/portfolio/portfolio.component.ts"],"names":[],"mappings":"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","sourcesContent":["// not really the Angular way:\n/* beautify preserve:start */\ndeclare var $;\ndeclare var Highcharts;\n/* beautify preserve:end */\n\nimport { Component, OnInit } from '@angular/core';\nimport { TAB_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\nimport { POPOVER_DIRECTIVES } from 'ng2-popover';\nimport { FORM_DIRECTIVES } from '@angular/forms/src/directives' // https://github.com/valor-software/ng2-bootstrap/issues/782\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\nimport { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\nimport { NG_TABLE_DIRECTIVES } from 'ng2-table/ng2-table';\nimport { HttpWrapperService } from '../http-wrapper.service';\nimport { Router } from '@angular/router';\nimport { Observable } from 'rxjs/Rx';\n\n@Component({\n moduleId: module.id,\n selector: 'app-portfolio',\n templateUrl: 'portfolio.component.html',\n styleUrls: [\n 'portfolio.component.css'\n ],\n directives: [POPOVER_DIRECTIVES, TAB_DIRECTIVES, NG_TABLE_DIRECTIVES, PAGINATION_DIRECTIVES, NgIf, CORE_DIRECTIVES, FORM_DIRECTIVES]\n})\n\nexport class PortfolioComponent implements OnInit {\n\n private IDFormatter(id) {\n return \"\" + id + \"\";\n }\n\n private defaultFormatter(value) {\n return value;\n }\n\n private numberFormatter(n) {\n if (!n) {\n return \"\";\n }\n\n var a = \"\" + n;\n a = a.replace(new RegExp(\"^(\\\\d{\" + (a.length % 3 ? a.length % 3 : 0) + \"})(\\\\d{3})\", \"g\"), \"$1 $2\").replace(/(\\d{3})+?/gi, \"$1 \").trim();\n var sep = \",\";\n a = a.replace(/\\s/g, sep);\n\n return a;\n }\n\n public rows: Array < any > = [];\n public columns: Array < any > = [\n // omitting the sort column on each column would result in no default sorting!\n { title: 'ID', name: 'id', sort: '', formatter: this.IDFormatter },\n { title: 'Product', name: 'product', sort: '', formatter: this.defaultFormatter },\n { title: 'Type', name: 'buySell', sort: '', formatter: this.defaultFormatter },\n { title: 'Trade Date', name: 'tradeDate', sort: 'desc', formatter: this.defaultFormatter },\n { title: 'Effective Date', name: 'effectiveDate', sort: '', formatter: this.defaultFormatter },\n { title: 'Maturity Date', name: 'maturityDate', sort: '', formatter: this.defaultFormatter },\n { title: 'Currency', name: 'currency', sort: '', formatter: this.defaultFormatter },\n { title: 'Notional', name: 'notional', sort: '', formatter: this.numberFormatter },\n { title: 'IM Contribution', name: 'im', sort: '', formatter: this.numberFormatter },\n { title: 'PV', name: 'mtm', sort: '', formatter: this.numberFormatter },\n { title: 'Included in summary', name: 'marginedText', sort: '', formatter: this.defaultFormatter }\n ];\n public page: number = 1;\n public itemsPerPage: number = 10;\n public maxSize: number = 5;\n public numPages: number = 1;\n public length: number = 0;\n\n public config: any = {\n paging: true,\n sorting: { columns: this.columns }\n };\n\n private businessDate: string;\n\n private data: Array < any > = [];\n\n private summaryTable: any = {\n product: \"Vanilla IRS\",\n currency: \"EUR\",\n trades: 0,\n notional: 0,\n im: 0,\n mtm: 0\n };\n\n private createTradesChart(TData) {\n var TFormat = 'Date: {point.x:%Y-%m-%d}
' + 'IM: {point.y:,.0f}€';\n\n $('#tradesChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n chart: {\n type: 'scatter',\n zoomType: 'xy'\n },\n rangeSelector: {\n selected: 4\n },\n title: {\n text: 'Individual Trades'\n },\n legend: {\n enabled: true\n },\n yAxis: {\n title: {\n text: 'IM'\n }\n },\n series: [{\n name: 'Trade',\n data: TData,\n tooltip: {\n pointFormat: TFormat\n }\n }]\n });\n }\n\n private createIMOverVMChart(IMVMData) {\n // note there's no \"highstocks\"\n $('#IMOverVMChart').highcharts({\n credits: {\n enabled: false\n },\n chart: {\n type: 'scatter',\n zoomType: 'xy'\n },\n title: {\n text: 'Imminent IM over Variation Margin of trades'\n },\n legend: {\n enabled: true\n },\n subtitle: {\n text: ''\n },\n xAxis: {\n title: {\n enabled: true,\n text: 'MTM'\n },\n startOnTick: true,\n endOnTick: true,\n showLastLabel: true\n },\n yAxis: {\n title: {\n text: 'IM'\n }\n },\n plotOptions: {\n scatter: {\n marker: {\n radius: 5,\n states: {\n hover: {\n enabled: true,\n lineColor: 'rgb(100,100,100)'\n }\n }\n },\n states: {\n hover: {\n marker: {\n enabled: false\n }\n }\n },\n tooltip: {\n headerFormat: '{series.name}
',\n pointFormat: 'IM: {point.x:,.0f}€
MTM: {point.x:,.0f}€
'\n }\n }\n },\n series: [{\n name: 'Trade',\n data: IMVMData\n }]\n\n });\n }\n\n private createIMVMHistoryChart(IMData, MTMData) {\n $('#IMVMHistoryChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n legend: {\n enabled: true\n },\n rangeSelector: {\n selected: 4\n },\n title: {\n text: 'Portfolio History'\n },\n subtitle: {\n text: 'Initial and Variation Margin Requirements'\n },\n xAxis: {\n type: 'datetime',\n dateTimeLabelFormats: { // don't display the dummy year\n //day: '%d'\n month: '%e. %b',\n year: '%b'\n },\n title: {\n text: 'Date'\n }\n },\n yAxis: {\n title: {\n text: 'Exposure (€)'\n },\n min: 0\n },\n plotOptions: {\n spline: {\n marker: {\n enabled: true\n }\n }\n },\n series: [{\n name: 'Initial Margin',\n data: IMData,\n type: 'column'\n }, {\n name: 'Mark to Market',\n data: MTMData,\n type: 'spline'\n }]\n });\n }\n\n private createActiveTradesChart(ATData) {\n var ATformat = 'Active trades: {point.y:,.0f}
' +\n 'IM: {point.x:,.0f}
';\n\n $('#activeTradesChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n rangeSelector: {\n selected: 4\n },\n legend: {\n enabled: true\n },\n xAxis: {\n type: 'datetime',\n dateTimeLabelFormats: { // don't display the dummy year\n //day: '%d'\n month: '%e. %b',\n year: '%b'\n },\n title: {\n text: 'Date'\n }\n },\n yAxis: {\n title: {\n text: 'Quantity'\n }\n },\n title: {\n text: 'Active Trades'\n },\n series: [{\n name: 'Active trades',\n data: ATData,\n tooltip: {\n pointFormat: ATformat\n }\n }]\n });\n }\n\n private createIMVMHistorySummaryChart(IMData, MTMData) {\n $('#IMVMHistorySummaryChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n rangeSelector: {\n enabled: false\n },\n navigator: {\n enabled: false\n },\n scrollbar: {\n enabled: false\n },\n title: {\n text: 'Portfolio History'\n },\n legend: {\n enabled: true\n },\n xAxis: {\n type: 'datetime',\n title: {\n text: 'Date'\n }\n },\n yAxis: {\n title: {\n text: 'Exposure (€)'\n },\n min: 0\n },\n plotOptions: {\n spline: {\n marker: {\n enabled: true\n }\n }\n },\n dataGrouping: {\n approximation: \"average\",\n enabled: true,\n forced: true,\n units: [\n ['month', [1]]\n ]\n },\n series: [{\n name: 'Initial Margin',\n data: IMData,\n type: 'column'\n }, {\n name: 'Mark to Market',\n data: MTMData,\n type: 'spline'\n }]\n });\n }\n\n constructor(private httpWrapperService: HttpWrapperService, private router: Router) {}\n\n private getData() {\n if (this.httpWrapperService.getCounterparty()) {\n // re-initialize addittive table sums\n this.summaryTable.trades = 0;\n this.summaryTable.notional = 0;\n this.summaryTable.im = 0;\n this.summaryTable.mtm = 0;\n\n this.data = null; //don't leave old data in case of errors\n\n //trades\n this.httpWrapperService.getWithCounterparty(\"trades\").toPromise().then((data) => {\n\n // trades over time scatter\n var TData = [];\n\n // trades IM over VM scatter\n var IMVMData = [];\n\n $.each(data, (index, value) => {\n if (value.margined) {\n TData.push([new Date(value.tradeDate).getTime(), value.im]);\n\n IMVMData.push([value.im, value.mtm]);\n }\n });\n\n this.createTradesChart(TData);\n\n this.createIMOverVMChart(IMVMData);\n\n // trades table\n this.data = data;\n this.length = this.data.length;\n this.onChangeTable(this.config);\n\n }).catch((error) => {\n console.log(\"Error loading trades\", error);\n });\n\n this.populateSummary().then(() => {\n // portfolio history and active trades charts\n this.httpWrapperService.getWithCounterparty(\"portfolio/history/aggregated\").toPromise().then((data) => {\n // summary table\n let lastDay = data\n this.summaryTable.trades = lastDay.activeTrades;\n this.summaryTable.notional = lastDay.notional;\n this.summaryTable.im = lastDay.im;\n this.summaryTable.mtm = lastDay.mtm;\n\n var IMData = [];\n var MTMData = [];\n var ATData = [];\n\n $.each(data, (index, value) => {\n // new Date(value.date).getTime() when dates are switched to YYYY-MM-DD\n IMData.push([value.date, value.im]);\n MTMData.push([value.date, value.mtm]);\n ATData.push([value.date, value.activeTrades]);\n });\n\n this.createIMVMHistoryChart(IMData, MTMData);\n this.createActiveTradesChart(ATData);\n\n this.createIMVMHistorySummaryChart(IMData, MTMData);\n }).catch((error) => {\n console.log(\"Error loading portfolio history\", error);\n })\n })\n }\n }\n\n private populateSummary() {\n return this.httpWrapperService.getWithCounterparty(\"portfolio/summary\").toPromise().then((data) => {\n this.summaryTable.trades = data.trades;\n this.summaryTable.notional = data.notional;\n }).catch((error) => {\n console.log(\"Error loading portfolio summary\", error);\n })\n }\n\n counterpartySubscription: any;\n ngOnInit() {\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\n this.businessDate = data.businessDate;\n }).catch((error) => {\n console.log(\"Error loading business date\", error);\n });\n\n Highcharts.setOptions({\n lang: {\n thousandsSep: ','\n }\n });\n\n this.getData();\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\n this.getData();\n });\n }\n\n ngOnDestroy() {\n this.counterpartySubscription.unsubscribe();\n }\n\n // table helper functions\n\n public changePage(page: any, data: Array < any > = this.data): Array < any > {\n let start = (page.page - 1) * page.itemsPerPage;\n let end = page.itemsPerPage > -1 ? (start + page.itemsPerPage) : data.length;\n return data.slice(start, end);\n }\n\n public changeSort(data: any, config: any): any {\n if (!config.sorting) {\n return data;\n }\n\n let columns = this.config.sorting.columns || [];\n let columnName: string = void 0;\n let sort: string = void 0;\n\n for (let i = 0; i < columns.length; i++) {\n if (columns[i].sort !== '') {\n columnName = columns[i].name;\n sort = columns[i].sort;\n }\n }\n\n if (!columnName) {\n return data;\n }\n\n // simple sorting\n return data.sort((previous: any, current: any) => {\n if (previous[columnName] > current[columnName]) {\n return sort === 'desc' ? -1 : 1;\n } else if (previous[columnName] < current[columnName]) {\n return sort === 'asc' ? -1 : 1;\n }\n return 0;\n });\n }\n\n public onChangeTable(config: any, page: any = { page: this.page, itemsPerPage: this.itemsPerPage }): any {\n if (config.sorting) {\n Object.assign(this.config.sorting, config.sorting);\n }\n\n let sortedData = this.changeSort(this.data, this.config);\n this.rows = page && config.paging ? this.changePage(page, sortedData) : sortedData;\n this.length = sortedData.length;\n }\n\n // end table helper functions\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map index 1d494c4c57..3a1779fd31 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"portfolio.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/portfolio/portfolio.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { PortfolioComponent } from './portfolio.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"portfolio.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/portfolio/portfolio.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { PortfolioComponent } from './portfolio.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map index f3a18d4b88..296ae2a6bb 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map index 4dbaf6f7e9..c2ce92ec00 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/valuations/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './valuations.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/valuations/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './valuations.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map index bfc06706b1..0285721250 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map @@ -1 +1 @@ -{"version":3,"file":"valuations.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/valuations/valuations.component.ts"],"names":[],"mappings":"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","sourcesContent":["/* beautify preserve:start */\r\ndeclare var $;\r\n/* beautify preserve:end */\r\n\r\nimport { Component, OnInit } from '@angular/core';\r\nimport { HttpWrapperService } from '../http-wrapper.service';\r\nimport { Observable } from 'rxjs/Rx';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-valuations',\r\n templateUrl: 'valuations.component.html',\r\n styleUrls: ['valuations.component.css'],\r\n directives: []\r\n})\r\nexport class ValuationsComponent implements OnInit {\r\n private data: any = {};\r\n private formattedData: any = {\r\n sensitivitiesCurves: []\r\n };\r\n private fullData: any = {};\r\n private businessDate: string;\r\n private timer;\r\n private timerSubscription;\r\n private counterpartySubscription;\r\n\r\n // show loading spinner when clicked and data is not all received\r\n private calculateClicked: boolean;\r\n\r\n private startCalculations() {\r\n console.log(\"Starting calculations\")\r\n this.fullData = {};\r\n this.data = {}; // outdated data, delete it\r\n this.calculateClicked = true; // show loading spinners\r\n\r\n // demo magic - this is to ensure we use the right valuation date\r\n this.httpWrapperService.postWithCounterparty(\"portfolio/valuations/calculate\", { valuationDate: \"2016-06-06\" } )\r\n .toPromise().then((data) => {\r\n this.fullData = data;\r\n this.businessDate = data.businessDate; // in case it's valuations for a different date now\r\n this.httpWrapperService.startDelayedTimer(); // demo magic\r\n this.getData();\r\n });\r\n }\r\n\r\n private getData() {\r\n this.data = this.httpWrapperService.getDelayedData(this.fullData);\r\n\r\n if (this.data && this.data.sensitivities) {\r\n this.formattedData.sensitivitiesCurves = this.getSensitivitiesCurves(this.data.sensitivities);\r\n }\r\n\r\n // scroll to bottom of page\r\n let spinners = document.getElementById(\"loadingSpinners\");\r\n if (spinners) {\r\n setTimeout(() => {\r\n $(\"html, body\").animate({ scrollTop: $(document).height() }, 1000);\r\n }, 100); // wait for spinners to have gone below latest element\r\n }\r\n }\r\n\r\n // TODO: make this independent from the actual curve names\r\n private getSensitivitiesCurves(sensitivities) {\r\n let formattedSensitivities = []; // formattedSensitivities\r\n\r\n // loop on the first curve, knowing that the other curves have the same values\r\n for (let key in sensitivities.curves[\"EUR-DSCON-BIMM\"]) {\r\n if (sensitivities.curves[\"EUR-DSCON-BIMM\"].hasOwnProperty(key)) {\r\n let obj = {\r\n tenor: key, //3M, 6M etc...\r\n \"EUR-DSCON-BIMM\": sensitivities.curves[\"EUR-DSCON-BIMM\"][key],\r\n \"EUR-EURIBOR3M-BIMM\": sensitivities.curves[\"EUR-EURIBOR3M-BIMM\"][key]\r\n };\r\n formattedSensitivities.push(obj);\r\n }\r\n }\r\n\r\n return formattedSensitivities;\r\n }\r\n\r\n constructor(private httpWrapperService: HttpWrapperService) {}\r\n\r\n ngOnInit() {\r\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\r\n this.businessDate = data.businessDate;\r\n }).catch((error) => {\r\n console.log(\"Error loading business date\", error);\r\n });\r\n\r\n // check for new data periodically\r\n // higher timeout because makes debugging annoying, put to 2000 for production\r\n this.timer = Observable.timer(0, 2000);\r\n this.timerSubscription = (this.timer.subscribe(() => this.getData()));\r\n\r\n // but also check for new data when counterparty changes\r\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\r\n this.getData();\r\n });\r\n\r\n }\r\n\r\n ngOnDestroy() {\r\n this.timerSubscription.unsubscribe();\r\n this.counterpartySubscription.unsubscribe();\r\n }\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"valuations.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/valuations/valuations.component.ts"],"names":[],"mappings":"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","sourcesContent":["/* beautify preserve:start */\ndeclare var $;\n/* beautify preserve:end */\n\nimport { Component, OnInit } from '@angular/core';\nimport { HttpWrapperService } from '../http-wrapper.service';\nimport { Observable } from 'rxjs/Rx';\n\n@Component({\n moduleId: module.id,\n selector: 'app-valuations',\n templateUrl: 'valuations.component.html',\n styleUrls: ['valuations.component.css'],\n directives: []\n})\nexport class ValuationsComponent implements OnInit {\n private data: any = {};\n private formattedData: any = {\n sensitivitiesCurves: []\n };\n private fullData: any = {};\n private businessDate: string;\n private timer;\n private timerSubscription;\n private counterpartySubscription;\n\n // show loading spinner when clicked and data is not all received\n private calculateClicked: boolean;\n\n private startCalculations() {\n console.log(\"Starting calculations\")\n this.fullData = {};\n this.data = {}; // outdated data, delete it\n this.calculateClicked = true; // show loading spinners\n\n // demo magic - this is to ensure we use the right valuation date\n this.httpWrapperService.postWithCounterparty(\"portfolio/valuations/calculate\", { valuationDate: \"2016-06-06\" } )\n .toPromise().then((data) => {\n this.fullData = data;\n this.businessDate = data.businessDate; // in case it's valuations for a different date now\n this.httpWrapperService.startDelayedTimer(); // demo magic\n this.getData();\n });\n }\n\n private getData() {\n this.data = this.httpWrapperService.getDelayedData(this.fullData);\n\n if (this.data && this.data.sensitivities) {\n this.formattedData.sensitivitiesCurves = this.getSensitivitiesCurves(this.data.sensitivities);\n }\n\n // scroll to bottom of page\n let spinners = document.getElementById(\"loadingSpinners\");\n if (spinners) {\n setTimeout(() => {\n $(\"html, body\").animate({ scrollTop: $(document).height() }, 1000);\n }, 100); // wait for spinners to have gone below latest element\n }\n }\n\n // TODO: make this independent from the actual curve names\n private getSensitivitiesCurves(sensitivities) {\n let formattedSensitivities = []; // formattedSensitivities\n\n // loop on the first curve, knowing that the other curves have the same values\n for (let key in sensitivities.curves[\"EUR-DSCON-BIMM\"]) {\n if (sensitivities.curves[\"EUR-DSCON-BIMM\"].hasOwnProperty(key)) {\n let obj = {\n tenor: key, //3M, 6M etc...\n \"EUR-DSCON-BIMM\": sensitivities.curves[\"EUR-DSCON-BIMM\"][key],\n \"EUR-EURIBOR3M-BIMM\": sensitivities.curves[\"EUR-EURIBOR3M-BIMM\"][key]\n };\n formattedSensitivities.push(obj);\n }\n }\n\n return formattedSensitivities;\n }\n\n constructor(private httpWrapperService: HttpWrapperService) {}\n\n ngOnInit() {\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\n this.businessDate = data.businessDate;\n }).catch((error) => {\n console.log(\"Error loading business date\", error);\n });\n\n // check for new data periodically\n // higher timeout because makes debugging annoying, put to 2000 for production\n this.timer = Observable.timer(0, 2000);\n this.timerSubscription = (this.timer.subscribe(() => this.getData()));\n\n // but also check for new data when counterparty changes\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\n this.getData();\n });\n\n }\n\n ngOnDestroy() {\n this.timerSubscription.unsubscribe();\n this.counterpartySubscription.unsubscribe();\n }\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map index 873daaea99..4fb63fd824 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"valuations.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/valuations/valuations.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,uBAAuB,EAAE;IAChC,EAAE,CAAC,2BAA2B,EAAE;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { ValuationsComponent } from './valuations.component';\r\n\r\ndescribe('Component: Valuations', () => {\r\n it('should create an instance', () => {\r\n });\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"valuations.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/valuations/valuations.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,uBAAuB,EAAE;IAChC,EAAE,CAAC,2BAA2B,EAAE;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { ValuationsComponent } from './valuations.component';\n\ndescribe('Component: Valuations', () => {\n it('should create an instance', () => {\n });\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map index 8268b70ed0..2dcf1c1386 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './view-trade.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './view-trade.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map index 46e066102d..52f64b1e74 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html index c17db9efcc..068a5b6708 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html @@ -27,10 +27,6 @@ Valuation Date {{deal.common.valuationDate}} - - Legal Document Hash - {{deal.common.hashLegalDocs}} - Interest Rates diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map index 2b28ce36f5..ec999b6a19 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map @@ -1 +1 @@ -{"version":3,"file":"view-trade.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/view-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAkD,iBAAiB,CAAC,CAAA;AAUpE;IAmBE,4BAAoB,WAAwB,EAAU,KAAqB;QAAvD,gBAAW,GAAX,WAAW,CAAa;QAAU,UAAK,GAAL,KAAK,CAAgB;QAlB3E,SAAI,GAAW;YACb,QAAQ,EAAE;gBACR,QAAQ,EAAE,EAAE;gBACZ,SAAS,EAAE,EAAE;gBACb,eAAe,EAAE,EAAE;aACpB;YACD,WAAW,EAAE;gBACX,QAAQ,EAAE,EAAE;gBACZ,eAAe,EAAE,EAAE;gBACnB,cAAc,EAAE,EAAE;aACnB;YACD,MAAM,EAAE;gBACN,YAAY,EAAE;oBACZ,KAAK,EAAE,EAAE;iBACV;aACF;SACF,CAAC;IAIF,CAAC;IAED,qCAAQ,GAAR;QAAA,iBAIC;QAHC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,UAAA,MAAM,IAAI,OAAA,MAAM,CAAC,SAAS,CAAC,EAAjB,CAAiB,CAAC,CAAC,SAAS,CAAC,UAAC,OAAO;YACnE,KAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qCAAQ,GAAR,UAAS,OAAe;QAAxB,iBAQC;QAPC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC;aAC9B,IAAI,CAAC,UAAC,IAAI;YACT,KAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;aACD,KAAK,CAAC,UAAC,GAAG;YACT,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACP,CAAC;IA7CH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,2BAA2B;YACxC,SAAS,EAAE,CAAC,sBAAsB,EAAE,0BAA0B,CAAC;YAC/D,SAAS,EAAE,CAAC,0BAAW,CAAC;YACxB,UAAU,EAAE,CAAC,0BAAiB,CAAC,CAAC,2BAA2B;SAC5D,CAAC;;0BAAA;IAuCF,yBAAC;AAAD,CAAC,AAtCD,IAsCC;AAtCY,0BAAkB,qBAsC9B,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\r\nimport { NodeService } from '../node.service';\r\nimport { ROUTER_DIRECTIVES, ActivatedRoute } from '@angular/router';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-view-trade',\r\n templateUrl: 'view-trade.component.html',\r\n styleUrls: ['../app.component.css', 'view-trade.component.css'],\r\n providers: [NodeService],\r\n directives: [ROUTER_DIRECTIVES] // necessary for routerLink\r\n})\r\nexport class ViewTradeComponent implements OnInit {\r\n deal: Object = {\r\n fixedLeg: {\r\n notional: {},\r\n fixedRate: {},\r\n paymentCalendar: {}\r\n },\r\n floatingLeg: {\r\n notional: {},\r\n paymentCalendar: {},\r\n fixingCalendar: {}\r\n },\r\n common: {\r\n interestRate: {\r\n tenor: {}\r\n }\r\n }\r\n };\r\n\r\n constructor(private nodeService: NodeService, private route: ActivatedRoute) {\r\n\r\n }\r\n\r\n ngOnInit() {\r\n this.route.params.map(params => params['tradeId']).subscribe((tradeId) => {\r\n this.showDeal(tradeId);\r\n });\r\n }\r\n\r\n showDeal(tradeId: string) {\r\n this.nodeService.getDeal(tradeId)\r\n .then((deal) => {\r\n this.deal = deal;\r\n })\r\n .catch((err) => {\r\n console.error(err);\r\n });\r\n }\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"view-trade.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/view-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAkD,iBAAiB,CAAC,CAAA;AAUpE;IAmBE,4BAAoB,WAAwB,EAAU,KAAqB;QAAvD,gBAAW,GAAX,WAAW,CAAa;QAAU,UAAK,GAAL,KAAK,CAAgB;QAlB3E,SAAI,GAAW;YACb,QAAQ,EAAE;gBACR,QAAQ,EAAE,EAAE;gBACZ,SAAS,EAAE,EAAE;gBACb,eAAe,EAAE,EAAE;aACpB;YACD,WAAW,EAAE;gBACX,QAAQ,EAAE,EAAE;gBACZ,eAAe,EAAE,EAAE;gBACnB,cAAc,EAAE,EAAE;aACnB;YACD,MAAM,EAAE;gBACN,YAAY,EAAE;oBACZ,KAAK,EAAE,EAAE;iBACV;aACF;SACF,CAAC;IAIF,CAAC;IAED,qCAAQ,GAAR;QAAA,iBAIC;QAHC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,UAAA,MAAM,IAAI,OAAA,MAAM,CAAC,SAAS,CAAC,EAAjB,CAAiB,CAAC,CAAC,SAAS,CAAC,UAAC,OAAO;YACnE,KAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qCAAQ,GAAR,UAAS,OAAe;QAAxB,iBAQC;QAPC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC;aAC9B,IAAI,CAAC,UAAC,IAAI;YACT,KAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;aACD,KAAK,CAAC,UAAC,GAAG;YACT,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACP,CAAC;IA7CH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,2BAA2B;YACxC,SAAS,EAAE,CAAC,sBAAsB,EAAE,0BAA0B,CAAC;YAC/D,SAAS,EAAE,CAAC,0BAAW,CAAC;YACxB,UAAU,EAAE,CAAC,0BAAiB,CAAC,CAAC,2BAA2B;SAC5D,CAAC;;0BAAA;IAuCF,yBAAC;AAAD,CAAC,AAtCD,IAsCC;AAtCY,0BAAkB,qBAsC9B,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\nimport { NodeService } from '../node.service';\nimport { ROUTER_DIRECTIVES, ActivatedRoute } from '@angular/router';\n\n@Component({\n moduleId: module.id,\n selector: 'app-view-trade',\n templateUrl: 'view-trade.component.html',\n styleUrls: ['../app.component.css', 'view-trade.component.css'],\n providers: [NodeService],\n directives: [ROUTER_DIRECTIVES] // necessary for routerLink\n})\nexport class ViewTradeComponent implements OnInit {\n deal: Object = {\n fixedLeg: {\n notional: {},\n fixedRate: {},\n paymentCalendar: {}\n },\n floatingLeg: {\n notional: {},\n paymentCalendar: {},\n fixingCalendar: {}\n },\n common: {\n interestRate: {\n tenor: {}\n }\n }\n };\n\n constructor(private nodeService: NodeService, private route: ActivatedRoute) {\n\n }\n\n ngOnInit() {\n this.route.params.map(params => params['tradeId']).subscribe((tradeId) => {\n this.showDeal(tradeId);\n });\n }\n\n showDeal(tradeId: string) {\n this.nodeService.getDeal(tradeId)\n .then((deal) => {\n this.deal = deal;\n })\n .catch((err) => {\n console.error(err);\n });\n }\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map index 464e5e8a25..e7fc9200f7 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"view-trade.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/view-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,EAAE,CAAC,2BAA2B,EAAE;QAC9B,2CAA2C;QAC3C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { ViewTradeComponent } from './view-trade.component';\r\n\r\ndescribe('Component: ViewTrade', () => {\r\n it('should create an instance', () => {\r\n //let component = new ViewTradeComponent();\r\n //expect(component).toBeTruthy();\r\n });\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"view-trade.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/view-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,EAAE,CAAC,2BAA2B,EAAE;QAC9B,2CAA2C;QAC3C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { ViewTradeComponent } from './view-trade.component';\n\ndescribe('Component: ViewTrade', () => {\n it('should create an instance', () => {\n //let component = new ViewTradeComponent();\n //expect(component).toBeTruthy();\n });\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js index 8aef2ed8a3..abd9c9a595 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js @@ -32,7 +32,6 @@ var CommonViewModel = (function () { this.exposure = {}; this.localBusinessDay = ["London", "NewYork"]; this.dailyInterestAmount = "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360"; - this.hashLegalDocs = "put hash here"; } return CommonViewModel; }()); diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map index 59371e1e98..643a5dbefa 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"CommonViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/CommonViewModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAG,KAAK,CAAC;QACrB,kBAAa,GAAG,YAAY,CAAC;QAC7B,oBAAe,GAAG,YAAY,CAAC;QAC/B,0BAAqB,GAAG,8BAA8B,CAAC;QACvD,uBAAkB,GAAG;YACjB,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,cAAS,GAAG;YACR,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,0BAAqB,GAAG;YACpB,QAAQ,EAAE,QAAQ;SACrB,CAAC;QACF,aAAQ,GAAG;YACP,QAAQ,EAAE,OAAO;SACpB,CAAC;QACF,kBAAa,GAAG,0BAA0B,CAAC;QAC3C,qBAAgB,GAAG,eAAe,CAAC;QACnC,mBAAc,GAAG,mGAAmG,CAAC;QACrH,iBAAY,GAAG;YACX,MAAM,EAAE,wBAAwB;YAChC,KAAK,EAAE;gBACH,IAAI,EAAE,IAAI;aACb;YACD,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,OAAO;SAChB,CAAC;QACF,wBAAmB,GAAG,EAAE,CAAC;QACzB,aAAQ,GAAG,EAAE,CAAC;QACd,qBAAgB,GAAG,CAAE,QAAQ,EAAG,SAAS,CAAE,CAAC;QAC5C,wBAAmB,GAAG,iGAAiG,CAAC;QACxH,kBAAa,GAAG,eAAe,CAAC;IAClC,CAAC;IAAD,sBAAC;AAAD,CAAC,AAjCD,IAiCC;AAjCY,uBAAe,kBAiC3B,CAAA","sourcesContent":["export class CommonViewModel {\r\n baseCurrency = \"EUR\";\r\n effectiveDate = \"2016-02-11\";\r\n terminationDate = \"2026-02-11\";\r\n eligibleCreditSupport = \"Cash in an Eligible Currency\";\r\n independentAmounts = {\r\n quantity: 0\r\n };\r\n threshold = {\r\n quantity: 0\r\n };\r\n minimumTransferAmount = {\r\n quantity: 25000000\r\n };\r\n rounding = {\r\n quantity: 1000000\r\n };\r\n valuationDate = \"Every Local Business Day\";\r\n notificationTime = \"2:00pm London\";\r\n resolutionTime = \"2:00pm London time on the first LocalBusiness Day following the date on which the notice is given\";\r\n interestRate = {\r\n oracle: \"Rates Service Provider\",\r\n tenor: {\r\n name: \"6M\"\r\n },\r\n ratioUnit: null,\r\n name: \"EONIA\"\r\n };\r\n addressForTransfers = \"\";\r\n exposure = {};\r\n localBusinessDay = [ \"London\" , \"NewYork\" ];\r\n dailyInterestAmount = \"(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360\";\r\n hashLegalDocs = \"put hash here\";\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"CommonViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/CommonViewModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAG,KAAK,CAAC;QACrB,kBAAa,GAAG,YAAY,CAAC;QAC7B,oBAAe,GAAG,YAAY,CAAC;QAC/B,0BAAqB,GAAG,8BAA8B,CAAC;QACvD,uBAAkB,GAAG;YACjB,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,cAAS,GAAG;YACR,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,0BAAqB,GAAG;YACpB,QAAQ,EAAE,QAAQ;SACrB,CAAC;QACF,aAAQ,GAAG;YACP,QAAQ,EAAE,OAAO;SACpB,CAAC;QACF,kBAAa,GAAG,0BAA0B,CAAC;QAC3C,qBAAgB,GAAG,eAAe,CAAC;QACnC,mBAAc,GAAG,mGAAmG,CAAC;QACrH,iBAAY,GAAG;YACX,MAAM,EAAE,wBAAwB;YAChC,KAAK,EAAE;gBACH,IAAI,EAAE,IAAI;aACb;YACD,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,OAAO;SAChB,CAAC;QACF,wBAAmB,GAAG,EAAE,CAAC;QACzB,aAAQ,GAAG,EAAE,CAAC;QACd,qBAAgB,GAAG,CAAE,QAAQ,EAAG,SAAS,CAAE,CAAC;QAC5C,wBAAmB,GAAG,iGAAiG,CAAC;IAC1H,CAAC;IAAD,sBAAC;AAAD,CAAC,AAhCD,IAgCC;AAhCY,uBAAe,kBAgC3B,CAAA","sourcesContent":["export class CommonViewModel {\n baseCurrency = \"EUR\";\n effectiveDate = \"2016-02-11\";\n terminationDate = \"2026-02-11\";\n eligibleCreditSupport = \"Cash in an Eligible Currency\";\n independentAmounts = {\n quantity: 0\n };\n threshold = {\n quantity: 0\n };\n minimumTransferAmount = {\n quantity: 25000000\n };\n rounding = {\n quantity: 1000000\n };\n valuationDate = \"Every Local Business Day\";\n notificationTime = \"2:00pm London\";\n resolutionTime = \"2:00pm London time on the first LocalBusiness Day following the date on which the notice is given\";\n interestRate = {\n oracle: \"Rates Service Provider\",\n tenor: {\n name: \"6M\"\n },\n ratioUnit: null,\n name: \"EONIA\"\n };\n addressForTransfers = \"\";\n exposure = {};\n localBusinessDay = [ \"London\" , \"NewYork\" ];\n dailyInterestAmount = \"(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360\";\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map index 629056981d..63db4778f5 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"DealViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/DealViewModel.ts"],"names":[],"mappings":";AAAA,kCAAkC,qBAClC,CAAC,CADsD;AACvD,qCAAqC,wBACrC,CAAC,CAD4D;AAC7D,gCAAgC,mBAEhC,CAAC,CAFkD;AAEnD;IACE;QAEA,aAAQ,GAAG,IAAI,qCAAiB,EAAE,CAAC;QACnC,gBAAW,GAAG,IAAI,2CAAoB,EAAE,CAAC;QACzC,WAAM,GAAG,IAAI,iCAAe,EAAE,CAAC;IAJhB,CAAC;IAKlB,oBAAC;AAAD,CAAC,AAND,IAMC;AANY,qBAAa,gBAMzB,CAAA","sourcesContent":["import { FixedLegViewModel } from './FixedLegViewModel'\r\nimport { FloatingLegViewModel } from './FloatingLegViewModel'\r\nimport { CommonViewModel } from './CommonViewModel'\r\n\r\nexport class DealViewModel {\r\n constructor() {}\r\n\r\n fixedLeg = new FixedLegViewModel();\r\n floatingLeg = new FloatingLegViewModel();\r\n common = new CommonViewModel();\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"DealViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/DealViewModel.ts"],"names":[],"mappings":";AAAA,kCAAkC,qBAClC,CAAC,CADsD;AACvD,qCAAqC,wBACrC,CAAC,CAD4D;AAC7D,gCAAgC,mBAEhC,CAAC,CAFkD;AAEnD;IACE;QAEA,aAAQ,GAAG,IAAI,qCAAiB,EAAE,CAAC;QACnC,gBAAW,GAAG,IAAI,2CAAoB,EAAE,CAAC;QACzC,WAAM,GAAG,IAAI,iCAAe,EAAE,CAAC;IAJhB,CAAC;IAKlB,oBAAC;AAAD,CAAC,AAND,IAMC;AANY,qBAAa,gBAMzB,CAAA","sourcesContent":["import { FixedLegViewModel } from './FixedLegViewModel'\nimport { FloatingLegViewModel } from './FloatingLegViewModel'\nimport { CommonViewModel } from './CommonViewModel'\n\nexport class DealViewModel {\n constructor() {}\n\n fixedLeg = new FixedLegViewModel();\n floatingLeg = new FloatingLegViewModel();\n common = new CommonViewModel();\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js index a491a2a5a1..9ac11fba23 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js @@ -17,4 +17,4 @@ var FixedLegViewModel = (function () { return FixedLegViewModel; }()); exports.FixedLegViewModel = FixedLegViewModel; -//# sourceMappingURL=FixedLegViewModel.js.map +//# sourceMappingURL=FixedLegViewModel.js.map \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map index 0d85256f93..654543c58d 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FixedLegViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/FixedLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,mBAAc,GAAG,QAAQ,CAAC;QAC1B,aAAQ,GAAW;YACf,QAAQ,EAAE,UAAU;SACvB,CAAC;QACF,qBAAgB,GAAG,YAAY,CAAC;QAGhC,cAAS,GAAG,OAAO,CAAC;QACpB,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,eAAU,GAAW,EAAE,CAAC;QACxB,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;IAftB,CAAC;IAgBnB,wBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,yBAAiB,oBAiB7B,CAAA","sourcesContent":["export class FixedLegViewModel {\r\n constructor() { }\r\n\r\n fixedRatePayer = \"Bank A\";\r\n notional: Object = {\r\n quantity: 2500000000\r\n };\r\n paymentFrequency = \"SemiAnnual\";\r\n effectiveDateAdjustment: any;\r\n terminationDateAdjustment: any;\r\n fixedRate = \"1.676\";\r\n dayCountBasis = \"ACT/360\";\r\n rollConvention = \"ModifiedFollowing\";\r\n dayInMonth: Number = 10;\r\n paymentRule = \"InArrears\";\r\n paymentDelay = \"0\";\r\n interestPeriodAdjustment = \"Adjusted\";\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FixedLegViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/FixedLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,mBAAc,GAAG,kCAAkC,CAAC;QACpD,aAAQ,GAAW;YACf,QAAQ,EAAE,UAAU;SACvB,CAAC;QACF,qBAAgB,GAAG,YAAY,CAAC;QAGhC,cAAS,GAAG,OAAO,CAAC;QACpB,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,eAAU,GAAW,EAAE,CAAC;QACxB,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;IAftB,CAAC;IAgBnB,wBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,yBAAiB,oBAiB7B,CAAA","sourcesContent":["export class FixedLegViewModel {\n constructor() { }\n\n fixedRatePayer = \"CN=Bank A,O=Bank A,L=London,C=GB\";\n notional: Object = {\n quantity: 2500000000\n };\n paymentFrequency = \"SemiAnnual\";\n effectiveDateAdjustment: any;\n terminationDateAdjustment: any;\n fixedRate = \"1.676\";\n dayCountBasis = \"ACT/360\";\n rollConvention = \"ModifiedFollowing\";\n dayInMonth: Number = 10;\n paymentRule = \"InArrears\";\n paymentDelay = \"0\";\n interestPeriodAdjustment = \"Adjusted\";\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js index 1d76ebb08b..c6f59c63af 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js @@ -25,4 +25,4 @@ var FloatingLegViewModel = (function () { return FloatingLegViewModel; }()); exports.FloatingLegViewModel = FloatingLegViewModel; -//# sourceMappingURL=FloatingLegViewModel.js.map +//# sourceMappingURL=FloatingLegViewModel.js.map \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map index 7b6dc2a0f1..f97a9987be 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FloatingLegViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/FloatingLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,sBAAiB,GAAG,QAAQ,CAAC;QAC7B,aAAQ,GAAW;YAChB,QAAQ,EAAE,UAAU;SACtB,CAAC;QACF,qBAAgB,GAAG,WAAW,CAAC;QAG/B,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,yBAAoB,GAAG,mBAAmB,CAAC;QAC3C,eAAU,GAAW,EAAE,CAAC;QACxB,oBAAe,GAAW,EAAE,CAAC;QAC7B,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;QACtC,uBAAkB,GAAW,CAAC,CAAC;QAC/B,cAAS,GAAG,WAAW,CAAC;QACxB,sBAAiB,GAAG,WAAW,CAAC;QAChC,gBAAW,GAAG,wBAAwB,CAAC;QACvC,eAAU,GAAG;YACV,IAAI,EAAE,IAAI;SACZ,CAAC;IAvBc,CAAC;IAwBnB,2BAAC;AAAD,CAAC,AAzBD,IAyBC;AAzBY,4BAAoB,uBAyBhC,CAAA","sourcesContent":["export class FloatingLegViewModel {\r\n constructor() { }\r\n\r\n floatingRatePayer = \"Bank B\";\r\n notional: Object = {\r\n quantity: 2500000000\r\n };\r\n paymentFrequency = \"Quarterly\";\r\n effectiveDateAdjustment: any;\r\n terminationDateAdjustment: any;\r\n dayCountBasis = \"ACT/360\";\r\n rollConvention = \"ModifiedFollowing\";\r\n fixingRollConvention = \"ModifiedFollowing\";\r\n dayInMonth: Number = 10;\r\n resetDayInMonth: Number = 10;\r\n paymentRule = \"InArrears\";\r\n paymentDelay = \"0\";\r\n interestPeriodAdjustment = \"Adjusted\";\r\n fixingPeriodOffset: Number = 2;\r\n resetRule = \"InAdvance\";\r\n fixingsPerPayment = \"Quarterly\";\r\n indexSource = \"Rates Service Provider\";\r\n indexTenor = {\r\n name: \"3M\"\r\n };\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FloatingLegViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/FloatingLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,sBAAiB,GAAG,oCAAoC,CAAC;QACzD,aAAQ,GAAW;YAChB,QAAQ,EAAE,UAAU;SACtB,CAAC;QACF,qBAAgB,GAAG,WAAW,CAAC;QAG/B,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,yBAAoB,GAAG,mBAAmB,CAAC;QAC3C,eAAU,GAAW,EAAE,CAAC;QACxB,oBAAe,GAAW,EAAE,CAAC;QAC7B,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;QACtC,uBAAkB,GAAW,CAAC,CAAC;QAC/B,cAAS,GAAG,WAAW,CAAC;QACxB,sBAAiB,GAAG,WAAW,CAAC;QAChC,gBAAW,GAAG,wBAAwB,CAAC;QACvC,eAAU,GAAG;YACV,IAAI,EAAE,IAAI;SACZ,CAAC;IAvBc,CAAC;IAwBnB,2BAAC;AAAD,CAAC,AAzBD,IAyBC;AAzBY,4BAAoB,uBAyBhC,CAAA","sourcesContent":["export class FloatingLegViewModel {\n constructor() { }\n\n floatingRatePayer = \"CN=Bank B,O=Bank B,L=New York,C=US\";\n notional: Object = {\n quantity: 2500000000\n };\n paymentFrequency = \"Quarterly\";\n effectiveDateAdjustment: any;\n terminationDateAdjustment: any;\n dayCountBasis = \"ACT/360\";\n rollConvention = \"ModifiedFollowing\";\n fixingRollConvention = \"ModifiedFollowing\";\n dayInMonth: Number = 10;\n resetDayInMonth: Number = 10;\n paymentRule = \"InArrears\";\n paymentDelay = \"0\";\n interestPeriodAdjustment = \"Adjusted\";\n fixingPeriodOffset: Number = 2;\n resetRule = \"InAdvance\";\n fixingsPerPayment = \"Quarterly\";\n indexSource = \"Rates Service Provider\";\n indexTenor = {\n name: \"3M\"\n };\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map index bb6428c60f..69b5d68d94 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map @@ -1 +1 @@ -{"version":3,"file":"main.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/main.ts"],"names":[],"mappings":";AAAA,yCAA0B,mCAAmC,CAAC,CAAA;AAC9D,qBAA+B,eAAe,CAAC,CAAA;AAC/C,qBAA+B,eAAe,CAAC,CAAA;AAC/C,sBAAmD,gBAAgB,CAAC,CAAA;AACpE,iBAA0C,QAAQ,CAAC,CAAA;AACnD,2BAAmC,kBAAkB,CAAC,CAAA;AAEtD,EAAE,CAAC,CAAC,cAAW,CAAC,UAAU,CAAC,CAAC,CAAC;IAC3B,qBAAc,EAAE,CAAC;AACnB,CAAC;AAED,oCAAS,CAAC,eAAY,EAAE;IACtB,+BAAkB;IAClB,qBAAc;IACd,+CAA+C;IAC/C,8BAAsB,EAAE;IACxB,oBAAY,EAAE;CACf,CAAC;KACD,KAAK,CAAC,UAAA,GAAG,IAAI,OAAA,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAlB,CAAkB,CAAC,CAAC","sourcesContent":["import { bootstrap } from '@angular/platform-browser-dynamic';\r\nimport { enableProdMode } from '@angular/core';\r\nimport { HTTP_PROVIDERS } from '@angular/http';\r\nimport {disableDeprecatedForms, provideForms} from '@angular/forms';\r\nimport { AppComponent, environment } from './app/';\r\nimport { appRouterProviders } from './app/app.routes';\r\n\r\nif (environment.production) {\r\n enableProdMode();\r\n}\r\n\r\nbootstrap(AppComponent, [\r\n appRouterProviders,\r\n HTTP_PROVIDERS,\r\n // magic to fix ngModel error on ng2-bootstrap:\r\n disableDeprecatedForms(),\r\n provideForms()\r\n])\r\n.catch(err => console.error(err));\r\n"]} \ No newline at end of file +{"version":3,"file":"main.js","sourceRoot":"","sources":["home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/main.ts"],"names":[],"mappings":";AAAA,yCAA0B,mCAAmC,CAAC,CAAA;AAC9D,qBAA+B,eAAe,CAAC,CAAA;AAC/C,qBAA+B,eAAe,CAAC,CAAA;AAC/C,sBAAmD,gBAAgB,CAAC,CAAA;AACpE,iBAA0C,QAAQ,CAAC,CAAA;AACnD,2BAAmC,kBAAkB,CAAC,CAAA;AAEtD,EAAE,CAAC,CAAC,cAAW,CAAC,UAAU,CAAC,CAAC,CAAC;IAC3B,qBAAc,EAAE,CAAC;AACnB,CAAC;AAED,oCAAS,CAAC,eAAY,EAAE;IACtB,+BAAkB;IAClB,qBAAc;IACd,+CAA+C;IAC/C,8BAAsB,EAAE;IACxB,oBAAY,EAAE;CACf,CAAC;KACD,KAAK,CAAC,UAAA,GAAG,IAAI,OAAA,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAlB,CAAkB,CAAC,CAAC","sourcesContent":["import { bootstrap } from '@angular/platform-browser-dynamic';\nimport { enableProdMode } from '@angular/core';\nimport { HTTP_PROVIDERS } from '@angular/http';\nimport {disableDeprecatedForms, provideForms} from '@angular/forms';\nimport { AppComponent, environment } from './app/';\nimport { appRouterProviders } from './app/app.routes';\n\nif (environment.production) {\n enableProdMode();\n}\n\nbootstrap(AppComponent, [\n appRouterProviders,\n HTTP_PROVIDERS,\n // magic to fix ngModel error on ng2-bootstrap:\n disableDeprecatedForms(),\n provideForms()\n])\n.catch(err => console.error(err));\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map index 09a6930934..ec7e385772 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map @@ -1 +1 @@ -{"version":3,"file":"system-config.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/system-config.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,8DAA8D;AAC9D,uCAAuC;AACvC,sEAAsE;AAEtE;;gGAEgG;AAChG,kCAAkC;AAClC,IAAM,GAAG,GAAQ;IACf,QAAQ,EAAE,yBAAyB;IACnC,YAAY,EAAE,iCAAiC;IAC/C,QAAQ,EAAE,uBAAuB;IACjC,YAAY,EAAE,oCAAoC;IAClD,eAAe,EAAE,sBAAsB;IACvC,aAAa,EAAE,oBAAoB;IACnC,YAAY,EAAE,mBAAmB;IACjC,WAAW,EAAE,kBAAkB;CAChC,CAAC;AAEF,mCAAmC;AACnC,IAAM,QAAQ,GAAQ;IACpB,QAAQ,EAAE;QACR,MAAM,EAAE,KAAK;KACd;IACD,YAAY,EAAE;QACZ,MAAM,EAAE,KAAK;KACd;IACD,eAAe,EAAE;QACf,MAAM,EAAE,KAAK;QACb,gBAAgB,EAAE,IAAI;QACtB,IAAI,EAAE,kBAAkB;KACzB;IACD,aAAa,EAAE;QACb,IAAI,EAAE,UAAU;QAChB,gBAAgB,EAAE,IAAI;KACvB;IACD,YAAY,EAAE;QACZ,gBAAgB,EAAE,IAAI;KACvB;IACD,WAAW,EAAE;QACX,gBAAgB,EAAE,IAAI;KACvB;CACF,CAAC;AAEF,gGAAgG;AAChG;;gGAEgG;AAChG,IAAM,OAAO,GAAa;IACxB,4BAA4B;IAC5B,eAAe;IACf,iBAAiB;IACjB,mBAAmB;IACnB,gBAAgB;IAChB,eAAe;IACf,iBAAiB;IACjB,2BAA2B;IAC3B,mCAAmC;IAEnC,sBAAsB;IACtB,MAAM;IAEN,wBAAwB;IACxB,KAAK;IACL,YAAY;IACZ,yBAAyB;IACzB,eAAe;IACf,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;CAEjB,CAAC;AAEF,IAAM,uBAAuB,GAAQ,EAAE,CAAC;AACxC,OAAO,CAAC,OAAO,CAAC,UAAC,UAAkB;IACjC,uBAAuB,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC1D,CAAC,CAAC,CAAC;AAKH,2BAA2B;AAE3B,wCAAwC;AACxC,MAAM,CAAC,MAAM,CAAC;IACZ,OAAO,EAAE,wBAAwB;IACjC,GAAG,EAAE;QACH,UAAU,EAAE,iBAAiB;QAC7B,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,SAAS;KAClB;IACD,QAAQ,EAAE,uBAAuB;CAClC,CAAC,CAAC;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAA,GAAG,EAAE,UAAA,QAAQ,EAAE,CAAC,CAAC","sourcesContent":["\"use strict\";\r\n\r\n// SystemJS configuration file, see links for more information\r\n// https://github.com/systemjs/systemjs\r\n// https://github.com/systemjs/systemjs/blob/master/docs/config-api.md\r\n\r\n/***********************************************************************************************\r\n * User Configuration.\r\n **********************************************************************************************/\r\n/** Map relative paths to URLs. */\r\nconst map: any = {\r\n 'moment': 'vendor/moment/moment.js',\r\n 'underscore': 'vendor/underscore/underscore.js',\r\n 'jquery': 'vendor/dist/jquery.js',\r\n 'highcharts': 'vendor/highcharts/highstock.src.js',\r\n 'ng2-bootstrap': 'vendor/ng2-bootstrap',\r\n 'ng2-popover': 'vendor/ng2-popover',\r\n 'ng2-select': 'vendor/ng2-select',\r\n 'ng2-table': 'vendor/ng2-table'\r\n};\r\n\r\n/** User packages configuration. */\r\nconst packages: any = {\r\n 'moment': {\r\n format: 'cjs'\r\n },\r\n 'underscore': {\r\n format: 'cjs'\r\n },\r\n 'ng2-bootstrap': {\r\n format: 'cjs',\r\n defaultExtension: 'js',\r\n main: 'ng2-bootstrap.js'\r\n },\r\n 'ng2-popover': {\r\n main: 'index.js',\r\n defaultExtension: 'js'\r\n },\r\n 'ng2-select': {\r\n defaultExtension: 'js'\r\n },\r\n 'ng2-table': {\r\n defaultExtension: 'js'\r\n }\r\n};\r\n\r\n////////////////////////////////////////////////////////////////////////////////////////////////\r\n/***************************************httpWrapperService********************************************************\r\n * Everything underneath this line is managed by the CLI.\r\n **********************************************************************************************/\r\nconst barrels: string[] = [\r\n // Angular specific barrels.\r\n '@angular/core',\r\n '@angular/common',\r\n '@angular/compiler',\r\n '@angular/forms',\r\n '@angular/http',\r\n '@angular/router',\r\n '@angular/platform-browser',\r\n '@angular/platform-browser-dynamic',\r\n\r\n // Thirdparty barrels.\r\n 'rxjs',\r\n\r\n // App specific barrels.\r\n 'app',\r\n 'app/shared',\r\n 'app/portfolio-component',\r\n 'app/portfolio',\r\n 'app/valuations',\r\n 'app/create-trade',\r\n 'app/view-trade',\r\n /** @cli-barrel */\r\n];\r\n\r\nconst cliSystemConfigPackages: any = {};\r\nbarrels.forEach((barrelName: string) => {\r\n cliSystemConfigPackages[barrelName] = { main: 'index' };\r\n});\r\n\r\n/** Type declaration for ambient System. */\r\n/* beautify preserve:start */\r\ndeclare var System: any;\r\n/* beautify preserve:end */\r\n\r\n// Apply the CLI SystemJS configuration.\r\nSystem.config({\r\n baseURL: \"/web/simmvaluationdemo\",\r\n map: {\r\n '@angular': 'vendor/@angular',\r\n 'rxjs': 'vendor/rxjs',\r\n 'main': 'main.js'\r\n },\r\n packages: cliSystemConfigPackages\r\n});\r\n\r\n// Apply the user's configuration.\r\nSystem.config({ map, packages });\r\n"]} \ No newline at end of file +{"version":3,"file":"system-config.js","sourceRoot":"","sources":["home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/system-config.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,8DAA8D;AAC9D,uCAAuC;AACvC,sEAAsE;AAEtE;;gGAEgG;AAChG,kCAAkC;AAClC,IAAM,GAAG,GAAQ;IACf,QAAQ,EAAE,yBAAyB;IACnC,YAAY,EAAE,iCAAiC;IAC/C,QAAQ,EAAE,uBAAuB;IACjC,YAAY,EAAE,oCAAoC;IAClD,eAAe,EAAE,sBAAsB;IACvC,aAAa,EAAE,oBAAoB;IACnC,YAAY,EAAE,mBAAmB;IACjC,WAAW,EAAE,kBAAkB;CAChC,CAAC;AAEF,mCAAmC;AACnC,IAAM,QAAQ,GAAQ;IACpB,QAAQ,EAAE;QACR,MAAM,EAAE,KAAK;KACd;IACD,YAAY,EAAE;QACZ,MAAM,EAAE,KAAK;KACd;IACD,eAAe,EAAE;QACf,MAAM,EAAE,KAAK;QACb,gBAAgB,EAAE,IAAI;QACtB,IAAI,EAAE,kBAAkB;KACzB;IACD,aAAa,EAAE;QACb,IAAI,EAAE,UAAU;QAChB,gBAAgB,EAAE,IAAI;KACvB;IACD,YAAY,EAAE;QACZ,gBAAgB,EAAE,IAAI;KACvB;IACD,WAAW,EAAE;QACX,gBAAgB,EAAE,IAAI;KACvB;CACF,CAAC;AAEF,gGAAgG;AAChG;;gGAEgG;AAChG,IAAM,OAAO,GAAa;IACxB,4BAA4B;IAC5B,eAAe;IACf,iBAAiB;IACjB,mBAAmB;IACnB,gBAAgB;IAChB,eAAe;IACf,iBAAiB;IACjB,2BAA2B;IAC3B,mCAAmC;IAEnC,sBAAsB;IACtB,MAAM;IAEN,wBAAwB;IACxB,KAAK;IACL,YAAY;IACZ,yBAAyB;IACzB,eAAe;IACf,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;CAEjB,CAAC;AAEF,IAAM,uBAAuB,GAAQ,EAAE,CAAC;AACxC,OAAO,CAAC,OAAO,CAAC,UAAC,UAAkB;IACjC,uBAAuB,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC1D,CAAC,CAAC,CAAC;AAKH,2BAA2B;AAE3B,wCAAwC;AACxC,MAAM,CAAC,MAAM,CAAC;IACZ,OAAO,EAAE,wBAAwB;IACjC,GAAG,EAAE;QACH,UAAU,EAAE,iBAAiB;QAC7B,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,SAAS;KAClB;IACD,QAAQ,EAAE,uBAAuB;CAClC,CAAC,CAAC;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAA,GAAG,EAAE,UAAA,QAAQ,EAAE,CAAC,CAAC","sourcesContent":["\"use strict\";\n\n// SystemJS configuration file, see links for more information\n// https://github.com/systemjs/systemjs\n// https://github.com/systemjs/systemjs/blob/master/docs/config-api.md\n\n/***********************************************************************************************\n * User Configuration.\n **********************************************************************************************/\n/** Map relative paths to URLs. */\nconst map: any = {\n 'moment': 'vendor/moment/moment.js',\n 'underscore': 'vendor/underscore/underscore.js',\n 'jquery': 'vendor/dist/jquery.js',\n 'highcharts': 'vendor/highcharts/highstock.src.js',\n 'ng2-bootstrap': 'vendor/ng2-bootstrap',\n 'ng2-popover': 'vendor/ng2-popover',\n 'ng2-select': 'vendor/ng2-select',\n 'ng2-table': 'vendor/ng2-table'\n};\n\n/** User packages configuration. */\nconst packages: any = {\n 'moment': {\n format: 'cjs'\n },\n 'underscore': {\n format: 'cjs'\n },\n 'ng2-bootstrap': {\n format: 'cjs',\n defaultExtension: 'js',\n main: 'ng2-bootstrap.js'\n },\n 'ng2-popover': {\n main: 'index.js',\n defaultExtension: 'js'\n },\n 'ng2-select': {\n defaultExtension: 'js'\n },\n 'ng2-table': {\n defaultExtension: 'js'\n }\n};\n\n////////////////////////////////////////////////////////////////////////////////////////////////\n/***************************************httpWrapperService********************************************************\n * Everything underneath this line is managed by the CLI.\n **********************************************************************************************/\nconst barrels: string[] = [\n // Angular specific barrels.\n '@angular/core',\n '@angular/common',\n '@angular/compiler',\n '@angular/forms',\n '@angular/http',\n '@angular/router',\n '@angular/platform-browser',\n '@angular/platform-browser-dynamic',\n\n // Thirdparty barrels.\n 'rxjs',\n\n // App specific barrels.\n 'app',\n 'app/shared',\n 'app/portfolio-component',\n 'app/portfolio',\n 'app/valuations',\n 'app/create-trade',\n 'app/view-trade',\n /** @cli-barrel */\n];\n\nconst cliSystemConfigPackages: any = {};\nbarrels.forEach((barrelName: string) => {\n cliSystemConfigPackages[barrelName] = { main: 'index' };\n});\n\n/** Type declaration for ambient System. */\n/* beautify preserve:start */\ndeclare var System: any;\n/* beautify preserve:end */\n\n// Apply the CLI SystemJS configuration.\nSystem.config({\n baseURL: \"/web/simmvaluationdemo\",\n map: {\n '@angular': 'vendor/@angular',\n 'rxjs': 'vendor/rxjs',\n 'main': 'main.js'\n },\n packages: cliSystemConfigPackages\n});\n\n// Apply the user's configuration.\nSystem.config({ map, packages });\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js index 4c5be4c0fb..644d35e274 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js @@ -1,4 +1,4 @@ -/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ -!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), -a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), -void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("