Docker system (end-to-end) test (#2437) (#2518)

* System test for IRS Demo utilizing docker, docker-compose and PhantomJS to automate full-stack testing

(cherry picked from commit a9856b9)
This commit is contained in:
Maksymilian Pawlak 2018-02-13 11:23:17 +01:00 committed by Katelyn Baker
parent 133bff90a6
commit 0b76c5d3f0
38 changed files with 829 additions and 472 deletions

View File

@ -65,6 +65,10 @@ buildscript {
ext.jsr305_version = constants.getProperty("jsr305Version")
ext.shiro_version = '1.4.0'
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
ext.snake_yaml_version = constants.getProperty('snakeYamlVersion')
ext.docker_compose_rule_version = '0.33.0'
ext.selenium_version = '3.8.1'
ext.ghostdriver_version = '2.1.0'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131'

View File

@ -1,8 +1,9 @@
gradlePluginsVersion=3.0.4
gradlePluginsVersion=3.0.5
kotlinVersion=1.1.60
platformVersion=2
guavaVersion=21.0
bouncycastleVersion=1.57
typesafeConfigVersion=1.3.1
jsr305Version=3.0.2
artifactoryPluginVersion=4.4.18
artifactoryPluginVersion=4.4.18
snakeYamlVersion=1.19

View File

@ -14,6 +14,7 @@ buildscript {
jsr305_version = constants.getProperty("jsr305Version")
kotlin_version = constants.getProperty("kotlinVersion")
artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion')
snake_yaml_version = constants.getProperty('snakeYamlVersion')
}
repositories {

View File

@ -20,7 +20,8 @@ public class CordformNode implements NodeDefinition {
protected static final String DEFAULT_HOST = "localhost";
/**
* Name of the node.
* Name of the node. Node will be placed in directory based on this name - all lowercase with whitespaces removed.
* Actual node name inside node.conf will be as set here.
*/
private String name;
@ -28,6 +29,20 @@ public class CordformNode implements NodeDefinition {
return name;
}
/**
* p2p Port.
*/
private int p2pPort = 10002;
public int getP2pPort() { return p2pPort; }
/**
* RPC Port.
*/
private int rpcPort = 10003;
public int getRpcPort() { return rpcPort; }
/**
* Set the RPC users for this node. This configuration block allows arbitrary configuration.
* The recommended current structure is:
@ -79,6 +94,7 @@ public class CordformNode implements NodeDefinition {
*/
public void p2pPort(int p2pPort) {
p2pAddress(DEFAULT_HOST + ':' + p2pPort);
this.p2pPort = p2pPort;
}
/**
@ -110,6 +126,7 @@ public class CordformNode implements NodeDefinition {
@Deprecated
public void rpcPort(int rpcPort) {
rpcAddress(DEFAULT_HOST + ':' + rpcPort);
this.rpcPort = rpcPort;
}
/**

View File

@ -8,6 +8,17 @@ public final class RpcSettings {
private Config config = ConfigFactory.empty();
private int port = 10003;
private int adminPort = 10005;
public int getPort() {
return port;
}
public int getAdminPort() {
return adminPort;
}
/**
* RPC address for the node.
*/
@ -15,6 +26,14 @@ public final class RpcSettings {
setValue("address", value);
}
/**
* RPC Port for the node
*/
public final void port(final int value) {
this.port = value;
setValue("address", "localhost:"+port);
}
/**
* RPC admin address for the node (necessary if [useSsl] is false or unset).
*/
@ -22,6 +41,11 @@ public final class RpcSettings {
setValue("adminAddress", value);
}
public final void adminPort(final int value) {
this.adminPort = value;
setValue("adminAddress", "localhost:"+adminPort);
}
/**
* Specifies whether the node RPC layer will require SSL from clients.
*/
@ -43,7 +67,7 @@ public final class RpcSettings {
config = options.addTo("ssl", config);
}
final Config addTo(final String key, final Config config) {
public final Config addTo(final String key, final Config config) {
if (this.config.isEmpty()) {
return config;
}

View File

@ -43,7 +43,7 @@ public final class SslOptions {
setValue("trustStoreFile", value);
}
final Config addTo(final String key, final Config config) {
public final Config addTo(final String key, final Config config) {
if (this.config.isEmpty()) {
return config;
}

View File

@ -40,6 +40,8 @@ dependencies {
noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
compile project(':cordform-common')
// Docker-compose file generation
compile "org.yaml:snakeyaml:$snake_yaml_version"
}
task createNodeRunner(type: Jar, dependsOn: [classes]) {

View File

@ -0,0 +1,174 @@
package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import java.util.jar.JarInputStream
/**
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
*
* See documentation for examples.
*/
@Suppress("unused")
open class Baseform : DefaultTask() {
private companion object {
val nodeJarName = "corda.jar"
private val defaultDirectory: Path = Paths.get("build", "nodes")
}
/**
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
*/
@Suppress("MemberVisibilityCanPrivate")
var definitionClass: String? = null
var directory = defaultDirectory
protected val nodes = mutableListOf<Node>()
/**
* Set the directory to install nodes into.
*
* @param directory The directory the nodes will be installed into.
*/
fun directory(directory: String) {
this.directory = Paths.get(directory)
}
/**
* Add a node configuration.
*
* @param configureClosure A node configuration that will be deployed.
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureClosure: Closure<in Node>) {
nodes += project.configure(Node(project), configureClosure) as Node
}
/**
* Add a node configuration
*
* @param configureFunc A node configuration that will be deployed
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureFunc: Node.() -> Any?): Node {
val node = Node(project).apply { configureFunc() }
nodes += node
return node
}
/**
* Returns a node by name.
*
* @param name The name of the node as specified in the node configuration DSL.
* @return A node instance.
*/
private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name }
/**
* The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadCordformDefinition(): CordformDefinition {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, CordformDefinition::class.java.classLoader)
.loadClass(definitionClass)
.asSubclass(CordformDefinition::class.java)
.newInstance()
}
/**
* The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadNetworkBootstrapperClass(): Class<*> {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper")
}
/**
* Installs the corda fat JAR to the root directory, for the network bootstrapper to use.
*/
protected fun installCordaJar() {
val cordaJar = Cordformation.verifyAndGetRuntimeJar(project, "corda")
project.copy {
it.apply {
from(cordaJar)
into(directory)
rename(cordaJar.name, nodeJarName)
fileMode = Cordformation.executableFileMode
}
}
}
protected fun initializeConfiguration() {
if (definitionClass != null) {
val cd = loadCordformDefinition()
// If the user has specified their own directory (even if it's the same default path) then let them know
// it's not used and should just rely on the one in CordformDefinition
require(directory === defaultDirectory) {
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
}
directory = cd.nodesDirectory
val cordapps = cd.getMatchingCordapps()
cd.nodeConfigurers.forEach {
val node = node { }
it.accept(node)
node.additionalCordapps.addAll(cordapps)
node.rootDir(directory)
}
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
} else {
nodes.forEach {
it.rootDir(directory)
}
}
}
protected fun bootstrapNetwork() {
val networkBootstrapperClass = loadNetworkBootstrapperClass()
val networkBootstrapper = networkBootstrapperClass.newInstance()
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
// Call NetworkBootstrapper.bootstrap
try {
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
bootstrapMethod.invoke(networkBootstrapper, rootDir)
} catch (e: InvocationTargetException) {
throw e.cause!!
}
}
private fun CordformDefinition.getMatchingCordapps(): List<File> {
val cordappJars = project.configuration("cordapp").files
return cordappPackages.map { `package` ->
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
when (cordappsWithPackage.size) {
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
1 -> cordappsWithPackage[0]
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
}
}
}
private fun File.containsPackage(`package`: String): Boolean {
JarInputStream(inputStream()).use {
while (true) {
val name = it.nextJarEntry?.name ?: break
if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) {
return true
}
}
return false
}
}
}

View File

@ -1,18 +1,12 @@
package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.lang.reflect.InvocationTargetException
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import java.util.jar.JarInputStream
/**
* Creates nodes based on the configuration of this task in the gradle configuration DSL.
@ -20,59 +14,12 @@ import java.util.jar.JarInputStream
* See documentation for examples.
*/
@Suppress("unused")
open class Cordform : DefaultTask() {
open class Cordform : Baseform() {
private companion object {
val nodeJarName = "corda.jar"
private val defaultDirectory: Path = Paths.get("build", "nodes")
}
/**
* Optionally the name of a CordformDefinition subclass to which all configuration will be delegated.
*/
@Suppress("MemberVisibilityCanPrivate")
var definitionClass: String? = null
private var directory = defaultDirectory
private val nodes = mutableListOf<Node>()
/**
* Set the directory to install nodes into.
*
* @param directory The directory the nodes will be installed into.
*/
fun directory(directory: String) {
this.directory = Paths.get(directory)
}
/**
* Add a node configuration.
*
* @param configureClosure A node configuration that will be deployed.
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureClosure: Closure<in Node>) {
nodes += project.configure(Node(project), configureClosure) as Node
}
/**
* Add a node configuration
*
* @param configureFunc A node configuration that will be deployed
*/
@Suppress("MemberVisibilityCanPrivate")
fun node(configureFunc: Node.() -> Any?): Node {
val node = Node(project).apply { configureFunc() }
nodes += node
return node
}
/**
* Returns a node by name.
*
* @param name The name of the node as specified in the node configuration DSL.
* @return A node instance.
*/
private fun getNodeByName(name: String): Node? = nodes.firstOrNull { it.name == name }
/**
* Installs the run script into the nodes directory.
*/
@ -103,29 +50,6 @@ open class Cordform : DefaultTask() {
}
}
/**
* The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadCordformDefinition(): CordformDefinition {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, CordformDefinition::class.java.classLoader)
.loadClass(definitionClass)
.asSubclass(CordformDefinition::class.java)
.newInstance()
}
/**
* The NetworkBootstrapper needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath.
*/
private fun loadNetworkBootstrapperClass(): Class<*> {
val plugin = project.convention.getPlugin(JavaPluginConvention::class.java)
val classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath
val urls = classpath.files.map { it.toURI().toURL() }.toTypedArray()
return URLClassLoader(urls, javaClass.classLoader).loadClass("net.corda.nodeapi.internal.network.NetworkBootstrapper")
}
/**
* This task action will create and install the nodes based on the node configurations added.
*/
@ -139,80 +63,4 @@ open class Cordform : DefaultTask() {
bootstrapNetwork()
nodes.forEach(Node::build)
}
/**
* Installs the corda fat JAR to the root directory, for the network bootstrapper to use.
*/
private fun installCordaJar() {
val cordaJar = Cordformation.verifyAndGetRuntimeJar(project, "corda")
project.copy {
it.apply {
from(cordaJar)
into(directory)
rename(cordaJar.name, nodeJarName)
fileMode = Cordformation.executableFileMode
}
}
}
private fun initializeConfiguration() {
if (definitionClass != null) {
val cd = loadCordformDefinition()
// If the user has specified their own directory (even if it's the same default path) then let them know
// it's not used and should just rely on the one in CordformDefinition
require(directory === defaultDirectory) {
"'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead."
}
directory = cd.nodesDirectory
val cordapps = cd.getMatchingCordapps()
cd.nodeConfigurers.forEach {
val node = node { }
it.accept(node)
node.additionalCordapps.addAll(cordapps)
node.rootDir(directory)
}
cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) }
} else {
nodes.forEach {
it.rootDir(directory)
}
}
}
private fun bootstrapNetwork() {
val networkBootstrapperClass = loadNetworkBootstrapperClass()
val networkBootstrapper = networkBootstrapperClass.newInstance()
val bootstrapMethod = networkBootstrapperClass.getMethod("bootstrap", Path::class.java).apply { isAccessible = true }
// Call NetworkBootstrapper.bootstrap
try {
val rootDir = project.projectDir.toPath().resolve(directory).toAbsolutePath().normalize()
bootstrapMethod.invoke(networkBootstrapper, rootDir)
} catch (e: InvocationTargetException) {
throw e.cause!!
}
}
private fun CordformDefinition.getMatchingCordapps(): List<File> {
val cordappJars = project.configuration("cordapp").files
return cordappPackages.map { `package` ->
val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) }
when (cordappsWithPackage.size) {
0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`")
1 -> cordappsWithPackage[0]
else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage")
}
}
}
private fun File.containsPackage(`package`: String): Boolean {
JarInputStream(inputStream()).use {
while (true) {
val name = it.nextJarEntry?.name ?: break
if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) {
return true
}
}
return false
}
}
}

View File

@ -0,0 +1,66 @@
package net.corda.plugins
import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction
import org.yaml.snakeyaml.DumperOptions
import java.nio.file.Path
import java.nio.file.Paths
import org.yaml.snakeyaml.Yaml
import java.nio.charset.StandardCharsets
import java.nio.file.Files
/**
* Creates docker-compose file and image definitions based on the configuration of this task in the gradle configuration DSL.
*
* See documentation for examples.
*/
@Suppress("unused")
open class Dockerform : Baseform() {
private companion object {
val nodeJarName = "corda.jar"
private val defaultDirectory: Path = Paths.get("build", "docker")
private val dockerComposeFileVersion = "3"
private val yamlOptions = DumperOptions().apply {
indent = 2
defaultFlowStyle = DumperOptions.FlowStyle.BLOCK
}
private val yaml = Yaml(yamlOptions)
}
private val directoryPath = project.projectDir.toPath().resolve(directory)
val dockerComposePath = directoryPath.resolve("docker-compose.yml")
/**
* This task action will create and install the nodes based on the node configurations added.
*/
@TaskAction
fun build() {
project.logger.info("Running Cordform task")
initializeConfiguration()
nodes.forEach(Node::installDockerConfig)
installCordaJar()
bootstrapNetwork()
nodes.forEach(Node::buildDocker)
// Transform nodes path the absolute ones
val services = nodes.map { it.containerName to mapOf(
"build" to directoryPath.resolve(it.nodeDir.name).toAbsolutePath().toString(),
"ports" to listOf(it.rpcPort)) }.toMap()
val dockerComposeObject = mapOf(
"version" to dockerComposeFileVersion,
"services" to services)
val dockerComposeContent = yaml.dump(dockerComposeObject)
Files.write(dockerComposePath, dockerComposeContent.toByteArray(StandardCharsets.UTF_8))
}
}

View File

@ -3,8 +3,10 @@ package net.corda.plugins
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.ConfigObject
import groovy.lang.Closure
import net.corda.cordform.CordformNode
import net.corda.cordform.RpcSettings
import org.gradle.api.Project
import java.io.File
import java.nio.charset.StandardCharsets
@ -34,6 +36,11 @@ class Node(private val project: Project) : CordformNode() {
private set
internal lateinit var rootDir: File
private set
internal lateinit var containerName: String
private set
internal var rpcSettings: RpcSettings = RpcSettings()
private set
/**
* Sets whether this node will use HTTPS communication.
@ -59,7 +66,7 @@ class Node(private val project: Project) : CordformNode() {
* Specifies RPC settings for the node.
*/
fun rpcSettings(configureClosure: Closure<in RpcSettings>) {
val rpcSettings = project.configure(RpcSettings(project), configureClosure) as RpcSettings
rpcSettings = project.configure(RpcSettings(), configureClosure) as RpcSettings
config = rpcSettings.addTo("rpcSettings", config)
}
@ -81,6 +88,19 @@ class Node(private val project: Project) : CordformNode() {
installCordapps()
}
internal fun buildDocker() {
project.copy {
it.apply {
from(Cordformation.getPluginFile(project, "net/corda/plugins/Dockerfile"))
from(Cordformation.getPluginFile(project, "net/corda/plugins/run-corda.sh"))
into("$nodeDir/")
}
}
installAgentJar()
installBuiltCordapp()
installCordapps()
}
internal fun rootDir(rootDir: Path) {
if (name == null) {
project.logger.error("Node has a null name - cannot create node")
@ -90,8 +110,9 @@ class Node(private val project: Project) : CordformNode() {
// with loading our custom X509EdDSAEngine.
val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=")
val dirName = organizationName ?: name
containerName = dirName.replace("\\s+".toRegex(), "-").toLowerCase()
this.rootDir = rootDir.toFile()
nodeDir = File(this.rootDir, dirName)
nodeDir = File(this.rootDir, dirName.replace("\\s", ""))
Files.createDirectories(nodeDir.toPath())
}
@ -156,14 +177,14 @@ class Node(private val project: Project) : CordformNode() {
}
}
private fun createTempConfigFile(): File {
private fun createTempConfigFile(configObject: ConfigObject): File {
val options = ConfigRenderOptions
.defaults()
.setOriginComments(false)
.setComments(false)
.setFormatted(true)
.setJson(false)
val configFileText = config.root().render(options).split("\n").toList()
val configFileText = configObject.render(options).split("\n").toList()
// Need to write a temporary file first to use the project.copy, which resolves directories correctly.
val tmpDir = File(project.buildDir, "tmp")
Files.createDirectories(tmpDir.toPath())
@ -178,7 +199,27 @@ class Node(private val project: Project) : CordformNode() {
*/
internal fun installConfig() {
configureProperties()
val tmpConfFile = createTempConfigFile()
val tmpConfFile = createTempConfigFile(config.root())
appendOptionalConfig(tmpConfFile)
project.copy {
it.apply {
from(tmpConfFile)
into(rootDir)
}
}
}
/**
* Installs the Dockerized configuration file to the root directory and detokenises it.
*/
internal fun installDockerConfig() {
configureProperties()
val dockerConf = config
.withValue("p2pAddress", ConfigValueFactory.fromAnyRef("$containerName:$p2pPort"))
.withValue("rpcSettings.address", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.port}"))
.withValue("rpcSettings.adminAddress", ConfigValueFactory.fromAnyRef("$containerName:${rpcSettings.adminPort}"))
.withValue("detectPublicIp", ConfigValueFactory.fromAnyRef(false))
val tmpConfFile = createTempConfigFile(dockerConf.root())
appendOptionalConfig(tmpConfFile)
project.copy {
it.apply {

View File

@ -1,59 +0,0 @@
package net.corda.plugins
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigValueFactory
import groovy.lang.Closure
import org.gradle.api.Project
class RpcSettings(private val project: Project) {
private var config: Config = ConfigFactory.empty()
/**
* RPC address for the node.
*/
fun address(value: String) {
config += "address" to value
}
/**
* RPC admin address for the node (necessary if [useSsl] is false or unset).
*/
fun adminAddress(value: String) {
config += "adminAddress" to value
}
/**
* Specifies whether the node RPC layer will require SSL from clients.
*/
fun useSsl(value: Boolean) {
config += "useSsl" to value
}
/**
* Specifies whether the RPC broker is separate from the node.
*/
fun standAloneBroker(value: Boolean) {
config += "standAloneBroker" to value
}
/**
* Specifies SSL certificates options for the RPC layer.
*/
fun ssl(configureClosure: Closure<in SslOptions>) {
val sslOptions = project.configure(SslOptions(), configureClosure) as SslOptions
config = sslOptions.addTo("ssl", config)
}
internal fun addTo(key: String, config: Config): Config {
if (this.config.isEmpty) {
return config
}
return config + (key to this.config.root())
}
}
internal operator fun Config.plus(entry: Pair<String, Any>): Config {
return withValue(entry.first, ConfigValueFactory.fromAnyRef(entry.second))
}

View File

@ -1,50 +0,0 @@
package net.corda.plugins
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
class SslOptions {
private var config: Config = ConfigFactory.empty()
/**
* Password for the keystore.
*/
fun keyStorePassword(value: String) {
config += "keyStorePassword" to value
}
/**
* Password for the truststore.
*/
fun trustStorePassword(value: String) {
config += "trustStorePassword" to value
}
/**
* Directory under which key stores are to be placed.
*/
fun certificatesDirectory(value: String) {
config += "certificatesDirectory" to value
}
/**
* Absolute path to SSL keystore. Default: "[certificatesDirectory]/sslkeystore.jks"
*/
fun sslKeystore(value: String) {
config += "sslKeystore" to value
}
/**
* Absolute path to SSL truststore. Default: "[certificatesDirectory]/truststore.jks"
*/
fun trustStoreFile(value: String) {
config += "trustStoreFile" to value
}
internal fun addTo(key: String, config: Config): Config {
if (this.config.isEmpty) {
return config
}
return config + (key to this.config.root())
}
}

View File

@ -0,0 +1,44 @@
# Base image from (http://phusion.github.io/baseimage-docker)
FROM openjdk:8u151-jre-alpine
ENV CORDA_VERSION=${BUILDTIME_CORDA_VERSION}
ENV JAVA_OPTIONS=${BUILDTIME_JAVA_OPTIONS}
# Set image labels
LABEL net.corda.version = ${CORDA_VERSION} \
maintainer = "<devops@r3.com>" \
vendor = "R3"
RUN apk upgrade --update && \
apk add --update --no-cache bash iputils && \
rm -rf /var/cache/apk/* && \
# Add user to run the app && \
addgroup corda && \
adduser -G corda -D -s /bin/bash corda && \
# Create /opt/corda directory && \
mkdir -p /opt/corda/plugins && \
mkdir -p /opt/corda/logs
# Copy corda files
ADD --chown=corda:corda corda.jar /opt/corda/corda.jar
ADD --chown=corda:corda node.conf /opt/corda/node.conf
ADD --chown=corda:corda network-parameters /opt/corda/
ADD --chown=corda:corda cordapps/ /opt/corda/cordapps
ADD --chown=corda:corda additional-node-infos/ /opt/corda/additional-node-infos
ADD --chown=corda:corda certificates/ /opt/corda/certificates
ADD --chown=corda:corda drivers/ /opt/corda/drivers
ADD --chown=corda:corda persistence* /opt/corda/
COPY run-corda.sh /run-corda.sh
RUN chmod +x /run-corda.sh && \
sync && \
chown -R corda:corda /opt/corda
# Working directory for Corda
WORKDIR /opt/corda
ENV HOME=/opt/corda
USER corda
# Start it
CMD ["/run-corda.sh"]

View File

@ -0,0 +1,10 @@
#!/bin/sh
# If variable not present use default values
: ${CORDA_HOME:=/opt/corda}
: ${JAVA_OPTIONS:=-Xmx512m}
export CORDA_HOME JAVA_OPTIONS
cd ${CORDA_HOME}
java $JAVA_OPTIONS -jar ${CORDA_HOME}/corda.jar 2>&1

View File

@ -35,3 +35,26 @@ view it.
*Note:* The IRS web UI currently has a bug when changing the clock time where it may show no numbers or apply fixings
inconsistently. The issues will be addressed in a future milestone release. Meanwhile, you can take a look at a simpler
oracle example here: https://github.com/corda/oracle-example.
## Running the system test
The system test utilize docker. Amount of RAM required to run the IRS system test is around 2.5GB, it is important
to allocated appropriate system resources (On MacOS/Windows this may require explicit changes to docker configuration)
### Gradle
The system test is designed to exercise the entire stack, including Corda nodes and the web frontend. It uses [Docker](https://www.docker.com), [docker-compose](https://docs.docker.com/compose/), and
[PhantomJS](http://phantomjs.org/). Docker and docker-compose need to be installed and configured to be inside the system path
(default installation). PhantomJs binary have to be put in a known location and have execution permission enabled
(``chmod a+x phantomjs`` on Unix) and the full path to the binary exposed as system property named ``phantomjs.binary.path`` or
a system variable named ``PHANTOMJS_BINARY_PATH``.
Having this done, the system test can be run by running the Gradle task ``:samples:irs-demo:systemTest``.
### Other
In order to run the the test by other means that the Gradle task - two more system properties are expected -
``CORDAPP_DOCKER_COMPOSE`` and ``WEB_DOCKER_COMPOSE`` which should specify full path docker-compose file for IRS cordapp
and web frontend respectively. Those can be obtained by running ``:samples:irs-demo:cordapp:prepareDockerNodes`` and
``web:generateDockerCompose`` Gradle tasks. ``systemTest`` task simply executes those two and set proper system properties up.

View File

@ -15,6 +15,7 @@ buildscript {
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
ext['artemis.version'] = "$artemis_version"
ext['hibernate.version'] = "$hibernate_version"
ext['selenium.version'] = "$selenium_version"
apply plugin: 'java'
apply plugin: 'kotlin'
@ -33,12 +34,29 @@ sourceSets {
srcDir file('src/integration-test/kotlin')
}
}
systemTest {
kotlin {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/system-test/kotlin')
}
}
}
configurations {
integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime
demoArtifacts.extendsFrom testRuntime
systemTestCompile.extendsFrom testCompile
}
repositories {
maven {
url 'https://dl.bintray.com/palantir/releases' // docker-compose-rule is published on bintray
}
repositories {
maven { url 'https://jitpack.io' }
}
}
dependencies {
@ -55,6 +73,9 @@ dependencies {
testCompile "org.assertj:assertj-core:${assertj_version}"
integrationTestCompile project(path: ":samples:irs-demo:web", configuration: "demoArtifacts")
testCompile "com.palantir.docker.compose:docker-compose-rule-junit4:$docker_compose_rule_version"
testCompile "org.seleniumhq.selenium:selenium-java:$selenium_version"
testCompile "com.github.detro:ghostdriver:$ghostdriver_version"
}
bootRepackage {
@ -66,6 +87,22 @@ task integrationTest(type: Test, dependsOn: []) {
classpath = sourceSets.integrationTest.runtimeClasspath
}
evaluationDependsOn("cordapp")
evaluationDependsOn("web")
task systemTest(type: Test, dependsOn: ["cordapp:prepareDockerNodes", "web:generateDockerCompose"]) {
testClassesDirs = sourceSets.systemTest.output.classesDirs
classpath = sourceSets.systemTest.runtimeClasspath
systemProperty "CORDAPP_DOCKER_COMPOSE", tasks.getByPath("cordapp:prepareDockerNodes").dockerComposePath.toString()
systemProperty "WEB_DOCKER_COMPOSE", tasks.getByPath("web:generateDockerCompose").dockerComposePath.toString()
def phantomJsPath = System.getProperty("phantomjs.binary.path") ?: System.getenv("PHANTOMJS_BINARY_PATH")
if (phantomJsPath != null) {
systemProperty "phantomjs.binary.path", phantomJsPath
}
}
idea {
module {
downloadJavadoc = true // defaults to false

View File

@ -43,63 +43,96 @@ dependencies {
testCompile "org.assertj:assertj-core:${assertj_version}"
}
def rpcUsersList = [
['username' : "user",
'password' : "password",
'permissions' : [
"StartFlow.net.corda.irs.flows.AutoOfferFlow\$Requester",
"StartFlow.net.corda.irs.flows.UpdateBusinessDayFlow\$Broadcast",
"StartFlow.net.corda.irs.api.NodeInterestRates\$UploadFixesFlow",
"InvokeRpc.vaultQueryBy",
"InvokeRpc.networkMapSnapshot",
"InvokeRpc.currentNodeTime",
"InvokeRpc.wellKnownPartyFromX500Name"
]]
]
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
ext.rpcUsers = [
['username' : "user",
'password' : "password",
'permissions' : [
"StartFlow.net.corda.irs.flows.AutoOfferFlow\$Requester",
"StartFlow.net.corda.irs.flows.UpdateBusinessDayFlow\$Broadcast",
"StartFlow.net.corda.irs.api.NodeInterestRates\$UploadFixesFlow",
"InvokeRpc.vaultQueryBy",
"InvokeRpc.networkMapSnapshot",
"InvokeRpc.currentNodeTime",
"InvokeRpc.wellKnownPartyFromX500Name"
]]
]
directory "./build/nodes"
node {
name "O=Notary Service,L=Zurich,C=CH"
notary = [validating : true]
p2pPort 10002
rpcSettings {
address "localhost:10003"
adminAddress "localhost:10023"
port 10003
adminPort 10023
}
cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
useTestClock true
}
node {
name "O=Bank A,L=London,C=GB"
p2pPort 10005
rpcSettings {
address "localhost:10006"
adminAddress "localhost:10026"
port 10006
adminPort 10026
}
cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
useTestClock true
}
node {
name "O=Bank B,L=New York,C=US"
p2pPort 10008
rpcSettings {
address "localhost:10009"
adminAddress "localhost:10029"
port 10009
adminPort 10029
}
cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
rpcUsers = rpcUsersList
useTestClock true
}
node {
name "O=Regulator,L=Moscow,C=RU"
p2pPort 10010
rpcPort 10011
rpcSettings {
port 10009
adminPort 10029
}
cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
useTestClock true
}
}
task prepareDockerNodes(type: net.corda.plugins.Dockerform, dependsOn: ['jar']) {
node {
name "O=Notary Service,L=Zurich,C=CH"
notary = [validating : true]
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
useTestClock true
}
node {
name "O=Bank A,L=London,C=GB"
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
useTestClock true
}
node {
name "O=Bank B,L=New York,C=US"
cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
useTestClock true
}
node {
name "O=Regulator,L=Moscow,C=RU"
cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList
useTestClock true
}
}

View File

@ -0,0 +1,80 @@
package net.corda.irs
import com.palantir.docker.compose.DockerComposeRule
import com.palantir.docker.compose.configuration.DockerComposeFiles
import com.palantir.docker.compose.connection.waiting.HealthChecks
import org.junit.ClassRule
import org.junit.Test
import org.openqa.selenium.By
import org.openqa.selenium.OutputType
import org.openqa.selenium.WebElement
import org.openqa.selenium.phantomjs.PhantomJSDriver
import org.openqa.selenium.support.ui.WebDriverWait
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class IRSDemoDockerTest {
companion object {
private fun ensureSystemVariable(variable: String) {
if (System.getProperty(variable) == null) {
throw IllegalStateException("System variable $variable not set. Please refer to README file for proper setup instructions.")
}
}
init {
ensureSystemVariable("CORDAPP_DOCKER_COMPOSE")
ensureSystemVariable("WEB_DOCKER_COMPOSE")
ensureSystemVariable("phantomjs.binary.path")
}
@ClassRule
@JvmField
var docker = DockerComposeRule.builder()
.files(DockerComposeFiles.from(
System.getProperty("CORDAPP_DOCKER_COMPOSE"),
System.getProperty("WEB_DOCKER_COMPOSE")))
.waitingForService("web-a", HealthChecks.toRespondOverHttp(8080, { port -> port.inFormat("http://\$HOST:\$EXTERNAL_PORT") }))
.build()
}
@Test
fun `runs IRS demo selenium phantomjs`() {
val driver = PhantomJSDriver()
val webAPort = docker.containers()
.container("web-a")
.port(8080)
driver.get("http://${webAPort.ip}:${webAPort.externalPort}");
//no deals on fresh interface
val dealRows = driver.findElementsByCssSelector("table#deal-list tbody tr")
assertTrue(dealRows.isEmpty())
// Click Angular link and wait for form to appear
val findElementByLinkText = driver.findElementByLinkText("Create Deal")
findElementByLinkText.click()
val driverWait = WebDriverWait(driver, 120)
val form = driverWait.until<WebElement>({
it?.findElement(By.cssSelector("form"))
})
form.submit()
//Wait for deals to appear in a rows table
val dealsList = driverWait.until<WebElement>({
it?.findElement(By.cssSelector("table#deal-list tbody tr"))
})
assertNotNull(dealsList)
}
}

View File

@ -1,17 +1,28 @@
import java.nio.charset.StandardCharsets
import java.nio.file.Files
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}")
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}")
classpath 'com.bmuschko:gradle-docker-plugin:3.2.1'
classpath "org.yaml:snakeyaml:1.19"
}
}
import org.yaml.snakeyaml.DumperOptions
plugins {
id 'com.craigburke.client-dependencies' version '1.4.0'
id 'com.craigburke.client-dependencies' version '1.4.0'
}
group = "${parent.group}.irs-demo"
clientDependencies {
registry 'realBower', type:'bower', url:'https://registry.bower.io'
realBower {
@ -69,6 +80,8 @@ jar {
dependsOn clientInstall
}
def docker_dir = file("$project.buildDir/docker")
task deployWebapps(type: Copy, dependsOn: ['jar', 'bootRepackage']) {
ext.webappDir = file("build/webapps")
@ -89,4 +102,58 @@ task demoJar(type: Jar) {
artifacts {
demoArtifacts demoJar
}
task createDockerfile(type: com.bmuschko.gradle.docker.tasks.image.Dockerfile, dependsOn: [bootRepackage]) {
destFile = file("$docker_dir/Dockerfile")
from 'azul/zulu-openjdk-alpine:8u152'
copyFile jar.archiveName, "/opt/irs/web/"
workingDir "/opt/irs/web/"
defaultCommand "sh", "-c", "java -Dcorda.host=\$CORDA_HOST -jar ${jar.archiveName}"
}
task prepareDockerDir(type: Copy, dependsOn: [bootRepackage, createDockerfile]) {
from jar
into docker_dir
}
task generateDockerCompose(dependsOn: [prepareDockerDir]) {
def outFile = new File(project.buildDir, "docker-compose.yml")
ext['dockerComposePath'] = outFile
doLast {
def dockerComposeObject = [
"version": "3",
"services": [
"web-a": [
"build": "$docker_dir".toString(),
"environment": [
"CORDA_HOST": "bank-a:10003"
],
"ports": ["8080"]
],
"web-b": [
"build": "$docker_dir".toString(),
"environment": [
"CORDA_HOST": "bank-b:10003"
],
"ports": ["8080"]
]
]
]
def options = new org.yaml.snakeyaml.DumperOptions()
options.indent = 2
options.defaultFlowStyle = DumperOptions.FlowStyle.BLOCK
def dockerComposeContent = new org.yaml.snakeyaml.Yaml(options).dump(dockerComposeObject)
Files.write(outFile.toPath(), dockerComposeContent.getBytes(StandardCharsets.UTF_8))
}
outputs.file(outFile)
}

View File

@ -6,6 +6,8 @@ import net.corda.client.rpc.CordaRPCClient
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.finance.plugin.registerFinanceJSONMappers
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.SpringApplication
@ -29,10 +31,22 @@ class IrsDemoWebApplication {
@Value("\${corda.password}")
lateinit var cordaPassword:String
@Bean
fun rpcClient(): CordaRPCOps {
return CordaRPCClient(NetworkHostAndPort.parse(cordaHost)).start(cordaUser, cordaPassword).proxy
log.info("Connecting to Corda on $cordaHost using username $cordaUser and password $cordaPassword")
// TODO remove this when CordaRPC gets proper connection retry, please
var maxRetries = 100;
do {
try {
return CordaRPCClient(NetworkHostAndPort.parse(cordaHost)).start(cordaUser, cordaPassword).proxy
} catch (ex: ActiveMQNotConnectedException) {
if (maxRetries-- > 0) {
Thread.sleep(1000)
} else {
throw ex
}
}
} while (true)
}
@Bean
@ -44,6 +58,8 @@ class IrsDemoWebApplication {
// running as standalone java app
companion object {
private val log = LoggerFactory.getLogger(this::class.java)
@JvmStatic fun main(args: Array<String>) {
SpringApplication.run(IrsDemoWebApplication::class.java, *args)
}

View File

@ -1,2 +1,2 @@
corda.host=localhost:10003
server.port=10004
server.port=10004

View File

@ -1,36 +1,32 @@
"use strict";
define(['viewmodel/FixedRate'], (fixedRateViewModel) => {
let calculationModel = {
define(['viewmodel/FixedRate'], function (fixedRateViewModel) {
var calculationModel = {
expression: "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
floatingLegPaymentSchedule: {
},
fixedLegPaymentSchedule: {
}
floatingLegPaymentSchedule: {},
fixedLegPaymentSchedule: {}
};
let indexLookup = {
var indexLookup = {
"GBP": "ICE LIBOR",
"USD": "ICE LIBOR",
"EUR": "EURIBOR"
};
let calendarLookup = {
var calendarLookup = {
"GBP": "London",
"USD": "NewYork",
"EUR": "London"
};
let Deal = function(dealViewModel) {
let now = new Date();
let tradeId = `T${now.getUTCFullYear()}-${now.getUTCMonth()}-${now.getUTCDate()}.${now.getUTCHours()}:${now.getUTCMinutes()}:${now.getUTCSeconds()}:${now.getUTCMilliseconds()}`;
var Deal = function Deal(dealViewModel) {
var now = new Date();
var tradeId = "T" + now.getUTCFullYear() + "-" + now.getUTCMonth() + "-" + now.getUTCDate() + "." + now.getUTCHours() + ":" + now.getUTCMinutes() + ":" + now.getUTCSeconds() + ":" + now.getUTCMilliseconds();
this.toJson = () => {
let fixedLeg = {};
let floatingLeg = {};
let common = {};
this.toJson = function () {
var fixedLeg = {};
var floatingLeg = {};
var common = {};
_.assign(fixedLeg, dealViewModel.fixedLeg);
_.assign(floatingLeg, dealViewModel.floatingLeg);
_.assign(common, dealViewModel.common);
@ -65,7 +61,7 @@ define(['viewmodel/FixedRate'], (fixedRateViewModel) => {
delete common.effectiveDate;
delete common.terminationDate;
let json = {
var json = {
fixedLeg: fixedLeg,
floatingLeg: floatingLeg,
calculation: calculationModel,
@ -77,4 +73,4 @@ define(['viewmodel/FixedRate'], (fixedRateViewModel) => {
};
};
return Deal;
});
});

View File

@ -2,23 +2,17 @@
function formatDateForNode(date) {
// Produces yyyy-dd-mm. JS is missing proper date formatting libs
let day = ("0" + (date.getDate())).slice(-2);
let month = ("0" + (date.getMonth() + 1)).slice(-2);
return `${date.getFullYear()}-${month}-${day}`;
var day = ("0" + date.getDate()).slice(-2);
var month = ("0" + (date.getMonth() + 1)).slice(-2);
return date.getFullYear() + "-" + month + "-" + day;
}
function formatDateForAngular(dateStr) {
let parts = dateStr.split("-");
var parts = dateStr.split("-");
return new Date(parts[0], parts[1], parts[2]);
}
define([
'angular',
'angularRoute',
'jquery',
'fcsaNumber',
'semantic'
], (angular, angularRoute, $, fcsaNumber, semantic) => {
define(['angular', 'angularRoute', 'jquery', 'fcsaNumber', 'semantic'], function (angular, angularRoute, $, fcsaNumber, semantic) {
angular.module('irsViewer', ['ngRoute', 'fcsa-number']);
requirejs(['routes']);
});

View File

@ -1,34 +1,27 @@
'use strict';
define([
'angular',
'maskedInput',
'utils/semantic',
'utils/dayCountBasisLookup',
'services/NodeApi',
'Deal',
'services/HttpErrorHandler'
], (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) => {
define(['angular', 'maskedInput', 'utils/semantic', 'utils/dayCountBasisLookup', 'services/NodeApi', 'Deal', 'services/HttpErrorHandler'], function (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) {
angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService, httpErrorHandler) {
semantic.init($scope, nodeService.isLoading);
let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
var handleHttpFail = httpErrorHandler.createErrorHandler($scope);
$scope.dayCountBasisLookup = dayCountBasisLookup;
$scope.deal = nodeService.newDeal();
$scope.createDeal = () => {
nodeService.createDeal(new Deal($scope.deal))
.then((tradeId) => $location.path('#/deal/' + tradeId), (resp) => {
$scope.createDeal = function () {
nodeService.createDeal(new Deal($scope.deal)).then(function (tradeId) {
return $location.path('#/deal/' + tradeId);
}, function (resp) {
$scope.formError = resp.data;
}, handleHttpFail);
};
$('input.percent').mask("9.999999", {placeholder: "", autoclear: false});
$('#swapirscolumns').click(() => {
let first = $('#irscolumns .irscolumn:eq( 0 )');
let last = $('#irscolumns .irscolumn:eq( 1 )');
$('input.percent').mask("9.999999", { placeholder: "", autoclear: false });
$('#swapirscolumns').click(function () {
var first = $('#irscolumns .irscolumn:eq( 0 )');
var last = $('#irscolumns .irscolumn:eq( 1 )');
first.before(last);
let swapPayers = () => {
let tmp = $scope.deal.floatingLeg.floatingRatePayer;
var swapPayers = function swapPayers() {
var tmp = $scope.deal.floatingLeg.floatingRatePayer;
$scope.deal.floatingLeg.floatingRatePayer = $scope.deal.fixedLeg.fixedRatePayer;
$scope.deal.fixedLeg.fixedRatePayer = tmp;
};

View File

@ -1,19 +1,21 @@
'use strict';
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (angular, semantic) => {
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], function (angular, semantic) {
angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService, httpErrorHandler) {
semantic.init($scope, nodeService.isLoading);
let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
let decorateDeal = (deal) => {
let paymentSchedule = deal.calculation.floatingLegPaymentSchedule;
Object.keys(paymentSchedule).map((key, index) => {
const sign = paymentSchedule[key].rate.positive ? 1 : -1;
paymentSchedule[key].ratePercent = paymentSchedule[key].rate.ratioUnit ? (paymentSchedule[key].rate.ratioUnit.value * 100 * sign).toFixed(5) + "%": "";
var handleHttpFail = httpErrorHandler.createErrorHandler($scope);
var decorateDeal = function decorateDeal(deal) {
var paymentSchedule = deal.calculation.floatingLegPaymentSchedule;
Object.keys(paymentSchedule).map(function (key, index) {
var sign = paymentSchedule[key].rate.positive ? 1 : -1;
paymentSchedule[key].ratePercent = paymentSchedule[key].rate.ratioUnit ? (paymentSchedule[key].rate.ratioUnit.value * 100 * sign).toFixed(5) + "%" : "";
});
return deal;
};
nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = decorateDeal(deal), handleHttpFail);
nodeService.getDeal($routeParams.dealId).then(function (deal) {
return $scope.deal = decorateDeal(deal);
}, handleHttpFail);
});
});

View File

@ -1,23 +1,23 @@
'use strict';
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (angular, semantic) => {
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], function (angular, semantic) {
angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService, httpErrorHandler) {
semantic.addLoadingModal($scope, nodeService.isLoading);
let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
var handleHttpFail = httpErrorHandler.createErrorHandler($scope);
$scope.infoMsg = "";
$scope.errorText = "";
$scope.date = { "year": "...", "month": "...", "day": "..." };
$scope.updateDate = (type) => {
nodeService.updateDate(type).then((newDate) => {
$scope.date = newDate
$scope.updateDate = function (type) {
nodeService.updateDate(type).then(function (newDate) {
$scope.date = newDate;
}, handleHttpFail);
};
/* Extract the common name from an X500 name */
$scope.renderX500Name = (x500Name) => {
var name = x500Name
x500Name.split(',').forEach(function(element) {
$scope.renderX500Name = function (x500Name) {
var name = x500Name;
x500Name.split(',').forEach(function (element) {
var keyValue = element.split('=');
if (keyValue[0].toUpperCase() == 'CN') {
name = keyValue[1];
@ -26,7 +26,11 @@ define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHand
return name;
};
nodeService.getDate().then((date) => $scope.date = date, handleHttpFail);
nodeService.getDeals().then((deals) => $scope.deals = deals, handleHttpFail);
nodeService.getDate().then(function (date) {
return $scope.date = date;
}, handleHttpFail);
nodeService.getDeals().then(function (deals) {
return $scope.deals = deals;
}, handleHttpFail);
});
});
});

View File

@ -11,18 +11,14 @@ require.config({
maskedInput: 'bower_components/jquery.maskedinput/jquery.maskedinput'
},
shim: {
'angular' : {'exports' : 'angular'},
'angular': { 'exports': 'angular' },
'angularRoute': ['angular'],
'fcsaNumber': ['angular'],
'semantic': ['jquery'],
'maskedInput': ['jquery']
},
priority: [
"angular"
],
baseUrl: 'js',
priority: ["angular"],
baseUrl: 'js'
});
require(['angular', 'app'], (angular, app) => {
});
require(['angular', 'app'], function (angular, app) {});

View File

@ -1,32 +1,22 @@
'use strict';
define([
'angular',
'controllers/Home',
'controllers/Deal',
'controllers/CreateDeal'
], (angular) => {
angular.module('irsViewer').config(($routeProvider, $locationProvider) => {
$routeProvider
.when('/', {
controller: 'HomeController',
templateUrl: 'view/home.html'
})
.when('/deal/:dealId', {
controller: 'DealController',
templateUrl: 'view/deal.html'
})
.when('/party/:partyId', {
templateUrl: 'view/party.html'
})
.when('/create-deal', {
controller: 'CreateDealController',
templateUrl: 'view/create-deal.html'
})
.otherwise({redirectTo: '/'});
define(['angular', 'controllers/Home', 'controllers/Deal', 'controllers/CreateDeal'], function (angular) {
angular.module('irsViewer').config(function ($routeProvider, $locationProvider) {
$routeProvider.when('/', {
controller: 'HomeController',
templateUrl: 'view/home.html'
}).when('/deal/:dealId', {
controller: 'DealController',
templateUrl: 'view/deal.html'
}).when('/party/:partyId', {
templateUrl: 'view/party.html'
}).when('/create-deal', {
controller: 'CreateDealController',
templateUrl: 'view/create-deal.html'
}).otherwise({ redirectTo: '/' });
});
angular.element().ready(function() {
angular.element().ready(function () {
// bootstrap the app manually
angular.bootstrap(document, ['irsViewer']);
});

View File

@ -1,11 +1,11 @@
'use strict';
define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _) => {
angular.module('irsViewer').factory('httpErrorHandler', () => {
define(['angular', 'lodash', 'viewmodel/Deal'], function (angular, _) {
angular.module('irsViewer').factory('httpErrorHandler', function () {
return {
createErrorHandler: (scope) => {
return (resp) => {
if(resp.status == -1) {
createErrorHandler: function createErrorHandler(scope) {
return function (resp) {
if (resp.status == -1) {
scope.httpError = "Could not connect to node web server";
} else {
scope.httpError = resp.data;

View File

@ -1,44 +1,48 @@
'use strict';
define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
angular.module('irsViewer').factory('nodeService', ($http) => {
return new (function() {
let date = new Date(2016, 0, 1, 0, 0, 0);
let curLoading = {};
let serverAddr = ''; // Leave empty to target the same host this page is served from
define(['angular', 'lodash', 'viewmodel/Deal'], function (angular, _, dealViewModel) {
angular.module('irsViewer').factory('nodeService', function ($http) {
return new function () {
var _this = this;
let load = (type, promise) => {
var date = new Date(2016, 0, 1, 0, 0, 0);
var curLoading = {};
var serverAddr = ''; // Leave empty to target the same host this page is served from
var load = function load(type, promise) {
curLoading[type] = true;
return promise.then((arg) => {
return promise.then(function (arg) {
curLoading[type] = false;
return arg;
}, (arg) => {
}, function (arg) {
curLoading[type] = false;
throw arg;
});
};
let endpoint = (target) => serverAddr + target;
var endpoint = function endpoint(target) {
return serverAddr + target;
};
let changeDateOnNode = (newDate) => {
const dateStr = formatDateForNode(newDate);
return load('date', $http.put(endpoint('/api/irs/demodate'), "\"" + dateStr + "\"")).then((resp) => {
var changeDateOnNode = function changeDateOnNode(newDate) {
var dateStr = formatDateForNode(newDate);
return load('date', $http.put(endpoint('/api/irs/demodate'), "\"" + dateStr + "\"")).then(function (resp) {
date = newDate;
return this.getDateModel(date);
return _this.getDateModel(date);
});
};
this.getDate = () => {
return load('date', $http.get(endpoint('/api/irs/demodate'))).then((resp) => {
const dateParts = resp.data;
this.getDate = function () {
return load('date', $http.get(endpoint('/api/irs/demodate'))).then(function (resp) {
var dateParts = resp.data;
date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); // JS uses 0 based months
return this.getDateModel(date);
return _this.getDateModel(date);
});
};
this.updateDate = (type) => {
let newDate = date;
switch(type) {
this.updateDate = function (type) {
var newDate = date;
switch (type) {
case "year":
newDate.setFullYear(date.getFullYear() + 1);
break;
@ -55,22 +59,22 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
return changeDateOnNode(newDate);
};
this.getDeals = () => {
return load('deals', $http.get(endpoint('/api/irs/deals'))).then((resp) => {
this.getDeals = function () {
return load('deals', $http.get(endpoint('/api/irs/deals'))).then(function (resp) {
return resp.data.reverse();
});
};
this.getDeal = (dealId) => {
return load('deal' + dealId, $http.get(endpoint('/api/irs/deals/' + dealId))).then((resp) => {
this.getDeal = function (dealId) {
return load('deal' + dealId, $http.get(endpoint('/api/irs/deals/' + dealId))).then(function (resp) {
// Do some data modification to simplify the model
let deal = resp.data;
var deal = resp.data;
deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.ratioUnit.value * 100).toString().slice(0, 6);
return deal;
});
};
this.getDateModel = (date) => {
this.getDateModel = function (date) {
return {
"year": date.getFullYear(),
"month": date.getMonth() + 1, // JS uses 0 based months
@ -78,24 +82,23 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
};
};
this.isLoading = () => {
return _.reduce(Object.keys(curLoading), (last, key) => {
return (last || curLoading[key]);
this.isLoading = function () {
return _.reduce(Object.keys(curLoading), function (last, key) {
return last || curLoading[key];
}, false);
};
this.newDeal = () => {
this.newDeal = function () {
return dealViewModel;
};
this.createDeal = (deal) => {
return load('create-deal', $http.post(endpoint('/api/irs/deals'), deal.toJson()))
.then((resp) => {
this.createDeal = function (deal) {
return load('create-deal', $http.post(endpoint('/api/irs/deals'), deal.toJson())).then(function (resp) {
return deal.tradeId;
}, (resp) => {
}, function (resp) {
throw resp;
})
}
});
});
};
}();
});
});

View File

@ -1,6 +1,6 @@
'use strict';
define([], () => {
define([], function () {
return {
"30/360": {
"day": "D30",
@ -29,6 +29,6 @@ define([], () => {
"ACT/ACT ICMA": {
"day": "DActual",
"year": "YICMA"
},
}
};
});

View File

@ -1,17 +1,17 @@
'use strict';
define(['jquery', 'semantic'], ($, semantic) => {
define(['jquery', 'semantic'], function ($, semantic) {
return {
init: function($scope, loadingFunc) {
init: function init($scope, loadingFunc) {
$('.ui.accordion').accordion();
$('.ui.dropdown').dropdown();
$('.ui.sticky').sticky();
this.addLoadingModal($scope, loadingFunc);
},
addLoadingModal: ($scope, loadingFunc) => {
$scope.$watch(loadingFunc, (newVal) => {
if(newVal === true) {
addLoadingModal: function addLoadingModal($scope, loadingFunc) {
$scope.$watch(loadingFunc, function (newVal) {
if (newVal === true) {
$('#loading').modal('setting', 'closable', false).modal('show');
} else {
$('#loading').modal('hide');

View File

@ -1,6 +1,6 @@
'use strict';
define([], () => {
define([], function () {
return {
baseCurrency: "USD",
effectiveDate: new Date(2016, 2, 11),
@ -31,7 +31,7 @@ define([], () => {
},
addressForTransfers: "",
exposure: {},
localBusinessDay: [ "London" , "NewYork" ],
localBusinessDay: ["London", "NewYork"],
dailyInterestAmount: "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360",
hashLegalDocs: "put hash here"
};

View File

@ -1,6 +1,6 @@
'use strict';
define(['viewmodel/FixedLeg', 'viewmodel/FloatingLeg', 'viewmodel/Common'], (fixedLeg, floatingLeg, common) => {
define(['viewmodel/FixedLeg', 'viewmodel/FloatingLeg', 'viewmodel/Common'], function (fixedLeg, floatingLeg, common) {
return {
fixedLeg: fixedLeg,
floatingLeg: floatingLeg,

View File

@ -1,6 +1,6 @@
'use strict';
define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => {
define(['utils/dayCountBasisLookup'], function (dayCountBasisLookup) {
return {
fixedRatePayer: "O=Bank A,L=London,C=GB",
notional: 2500000000,
@ -15,4 +15,4 @@ define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => {
paymentDelay: "0",
interestPeriodAdjustment: "Adjusted"
};
});
});

View File

@ -1,6 +1,6 @@
'use strict';
define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => {
define(['utils/dayCountBasisLookup'], function (dayCountBasisLookup) {
return {
floatingRatePayer: "O=Bank B,L=New York,C=US",
notional: 2500000000,
@ -20,7 +20,7 @@ define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => {
fixingsPerPayment: "Quarterly",
indexSource: "Rates Service Provider",
indexTenor: {
name: "3M"
name: "3M"
}
};
});
});

View File

@ -34,7 +34,7 @@
<i class="browser icon"></i>
Recent deals
</h3>
<table class="ui striped table">
<table class="ui striped table" id="deal-list">
<thead>
<tr class="center aligned">
<th>Trade Id</th>
@ -45,7 +45,7 @@
</tr>
</thead>
<tbody>
<tr class="center aligned" ng-repeat="deal in deals">
<tr class="center aligned" ng-repeat="deal in deals" id="deal-{{deal.ref}}">
<td><a href="#/deal/{{deal.ref}}">{{deal.ref}}</a></td>
<td class="single line">{{renderX500Name(deal.fixedLeg.fixedRatePayer)}}</td>
<td class="single line">{{deal.fixedLeg.notional}}</td>