Docker system (end-to-end) test ()

* System test for IRS Demo utilizing docker, docker-compose and PhantomJS to automate full-stack testing
This commit is contained in:
Maksymilian Pawlak 2018-02-05 11:42:20 +00:00 committed by GitHub
parent a08d333d5b
commit a9856b9ce6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 829 additions and 472 deletions

@ -65,6 +65,10 @@ buildscript {
ext.jsr305_version = constants.getProperty("jsr305Version") ext.jsr305_version = constants.getProperty("jsr305Version")
ext.shiro_version = '1.4.0' ext.shiro_version = '1.4.0'
ext.artifactory_plugin_version = constants.getProperty('artifactoryPluginVersion') 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: // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131' ext.java8_minUpdateVersion = '131'

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

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

@ -20,7 +20,8 @@ public class CordformNode implements NodeDefinition {
protected static final String DEFAULT_HOST = "localhost"; 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; private String name;
@ -28,6 +29,20 @@ public class CordformNode implements NodeDefinition {
return name; 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. * Set the RPC users for this node. This configuration block allows arbitrary configuration.
* The recommended current structure is: * The recommended current structure is:
@ -79,6 +94,7 @@ public class CordformNode implements NodeDefinition {
*/ */
public void p2pPort(int p2pPort) { public void p2pPort(int p2pPort) {
p2pAddress(DEFAULT_HOST + ':' + p2pPort); p2pAddress(DEFAULT_HOST + ':' + p2pPort);
this.p2pPort = p2pPort;
} }
/** /**
@ -110,6 +126,7 @@ public class CordformNode implements NodeDefinition {
@Deprecated @Deprecated
public void rpcPort(int rpcPort) { public void rpcPort(int rpcPort) {
rpcAddress(DEFAULT_HOST + ':' + rpcPort); rpcAddress(DEFAULT_HOST + ':' + rpcPort);
this.rpcPort = rpcPort;
} }
/** /**

@ -8,6 +8,17 @@ public final class RpcSettings {
private Config config = ConfigFactory.empty(); 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. * RPC address for the node.
*/ */
@ -15,6 +26,14 @@ public final class RpcSettings {
setValue("address", value); 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). * RPC admin address for the node (necessary if [useSsl] is false or unset).
*/ */
@ -22,6 +41,11 @@ public final class RpcSettings {
setValue("adminAddress", value); 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. * Specifies whether the node RPC layer will require SSL from clients.
*/ */
@ -43,7 +67,7 @@ public final class RpcSettings {
config = options.addTo("ssl", config); 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()) { if (this.config.isEmpty()) {
return config; return config;
} }

@ -43,7 +43,7 @@ public final class SslOptions {
setValue("trustStoreFile", value); 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()) { if (this.config.isEmpty()) {
return config; return config;
} }

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

@ -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
}
}
}

@ -1,18 +1,12 @@
package net.corda.plugins package net.corda.plugins
import groovy.lang.Closure
import net.corda.cordform.CordformDefinition
import org.apache.tools.ant.filters.FixCrLfFilter import org.apache.tools.ant.filters.FixCrLfFilter
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.api.tasks.TaskAction 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.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.jar.JarInputStream
/** /**
* Creates nodes based on the configuration of this task in the gradle configuration DSL. * 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. * See documentation for examples.
*/ */
@Suppress("unused") @Suppress("unused")
open class Cordform : DefaultTask() { open class Cordform : Baseform() {
private companion object { private companion object {
val nodeJarName = "corda.jar" val nodeJarName = "corda.jar"
private val defaultDirectory: Path = Paths.get("build", "nodes") 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. * 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. * This task action will create and install the nodes based on the node configurations added.
*/ */
@ -139,80 +63,4 @@ open class Cordform : DefaultTask() {
bootstrapNetwork() bootstrapNetwork()
nodes.forEach(Node::build) 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
}
}
} }

@ -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))
}
}

@ -3,8 +3,10 @@ package net.corda.plugins
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.ConfigObject
import groovy.lang.Closure import groovy.lang.Closure
import net.corda.cordform.CordformNode import net.corda.cordform.CordformNode
import net.corda.cordform.RpcSettings
import org.gradle.api.Project import org.gradle.api.Project
import java.io.File import java.io.File
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@ -34,6 +36,11 @@ class Node(private val project: Project) : CordformNode() {
private set private set
internal lateinit var rootDir: File internal lateinit var rootDir: File
private set private set
internal lateinit var containerName: String
private set
internal var rpcSettings: RpcSettings = RpcSettings()
private set
/** /**
* Sets whether this node will use HTTPS communication. * 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. * Specifies RPC settings for the node.
*/ */
fun rpcSettings(configureClosure: Closure<in RpcSettings>) { 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) config = rpcSettings.addTo("rpcSettings", config)
} }
@ -81,6 +88,19 @@ class Node(private val project: Project) : CordformNode() {
installCordapps() 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) { internal fun rootDir(rootDir: Path) {
if (name == null) { if (name == null) {
project.logger.error("Node has a null name - cannot create node") 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. // with loading our custom X509EdDSAEngine.
val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=") val organizationName = name.trim().split(",").firstOrNull { it.startsWith("O=") }?.substringAfter("=")
val dirName = organizationName ?: name val dirName = organizationName ?: name
containerName = dirName.replace("\\s+".toRegex(), "-").toLowerCase()
this.rootDir = rootDir.toFile() this.rootDir = rootDir.toFile()
nodeDir = File(this.rootDir, dirName) nodeDir = File(this.rootDir, dirName.replace("\\s", ""))
Files.createDirectories(nodeDir.toPath()) 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 val options = ConfigRenderOptions
.defaults() .defaults()
.setOriginComments(false) .setOriginComments(false)
.setComments(false) .setComments(false)
.setFormatted(true) .setFormatted(true)
.setJson(false) .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. // Need to write a temporary file first to use the project.copy, which resolves directories correctly.
val tmpDir = File(project.buildDir, "tmp") val tmpDir = File(project.buildDir, "tmp")
Files.createDirectories(tmpDir.toPath()) Files.createDirectories(tmpDir.toPath())
@ -178,7 +199,27 @@ class Node(private val project: Project) : CordformNode() {
*/ */
internal fun installConfig() { internal fun installConfig() {
configureProperties() 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) appendOptionalConfig(tmpConfFile)
project.copy { project.copy {
it.apply { it.apply {

@ -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))
}

@ -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())
}
}

@ -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"]

@ -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

@ -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 *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 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. 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.

@ -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 // 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['artemis.version'] = "$artemis_version"
ext['hibernate.version'] = "$hibernate_version" ext['hibernate.version'] = "$hibernate_version"
ext['selenium.version'] = "$selenium_version"
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'kotlin' apply plugin: 'kotlin'
@ -33,12 +34,29 @@ sourceSets {
srcDir file('src/integration-test/kotlin') 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 { configurations {
integrationTestCompile.extendsFrom testCompile integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime integrationTestRuntime.extendsFrom testRuntime
demoArtifacts.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 { dependencies {
@ -55,6 +73,9 @@ dependencies {
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"
integrationTestCompile project(path: ":samples:irs-demo:web", configuration: "demoArtifacts") 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 { bootRepackage {
@ -66,6 +87,22 @@ task integrationTest(type: Test, dependsOn: []) {
classpath = sourceSets.integrationTest.runtimeClasspath 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 { idea {
module { module {
downloadJavadoc = true // defaults to false downloadJavadoc = true // defaults to false

@ -43,63 +43,96 @@ dependencies {
testCompile "org.assertj:assertj-core:${assertj_version}" 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']) { 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 { node {
name "O=Notary Service,L=Zurich,C=CH" name "O=Notary Service,L=Zurich,C=CH"
notary = [validating : true] notary = [validating : true]
p2pPort 10002 p2pPort 10002
rpcSettings { rpcSettings {
address "localhost:10003" port 10003
adminAddress "localhost:10023" adminPort 10023
} }
cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers rpcUsers = rpcUsersList
useTestClock true useTestClock true
} }
node { node {
name "O=Bank A,L=London,C=GB" name "O=Bank A,L=London,C=GB"
p2pPort 10005 p2pPort 10005
rpcSettings { rpcSettings {
address "localhost:10006" port 10006
adminAddress "localhost:10026" adminPort 10026
} }
cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers rpcUsers = rpcUsersList
useTestClock true useTestClock true
} }
node { node {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
p2pPort 10008 p2pPort 10008
rpcSettings { rpcSettings {
address "localhost:10009" port 10009
adminAddress "localhost:10029" adminPort 10029
} }
cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = ext.rpcUsers rpcUsers = rpcUsersList
useTestClock true useTestClock true
} }
node { node {
name "O=Regulator,L=Moscow,C=RU" name "O=Regulator,L=Moscow,C=RU"
p2pPort 10010 p2pPort 10010
rpcPort 10011 rpcSettings {
port 10009
adminPort 10029
}
cordapps = ["${project.group}:finance:$corda_release_version"] 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 useTestClock true
} }
} }

@ -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)
}
}

@ -1,17 +1,28 @@
import java.nio.charset.StandardCharsets
import java.nio.file.Files
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}") 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 { 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 { clientDependencies {
registry 'realBower', type:'bower', url:'https://registry.bower.io' registry 'realBower', type:'bower', url:'https://registry.bower.io'
realBower { realBower {
@ -69,6 +80,8 @@ jar {
dependsOn clientInstall dependsOn clientInstall
} }
def docker_dir = file("$project.buildDir/docker")
task deployWebapps(type: Copy, dependsOn: ['jar', 'bootRepackage']) { task deployWebapps(type: Copy, dependsOn: ['jar', 'bootRepackage']) {
ext.webappDir = file("build/webapps") ext.webappDir = file("build/webapps")
@ -89,4 +102,58 @@ task demoJar(type: Jar) {
artifacts { artifacts {
demoArtifacts demoJar 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)
} }

@ -6,6 +6,8 @@ import net.corda.client.rpc.CordaRPCClient
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.finance.plugin.registerFinanceJSONMappers 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.Autowired
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.SpringApplication import org.springframework.boot.SpringApplication
@ -29,10 +31,22 @@ class IrsDemoWebApplication {
@Value("\${corda.password}") @Value("\${corda.password}")
lateinit var cordaPassword:String lateinit var cordaPassword:String
@Bean @Bean
fun rpcClient(): CordaRPCOps { 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 @Bean
@ -44,6 +58,8 @@ class IrsDemoWebApplication {
// running as standalone java app // running as standalone java app
companion object { companion object {
private val log = LoggerFactory.getLogger(this::class.java)
@JvmStatic fun main(args: Array<String>) { @JvmStatic fun main(args: Array<String>) {
SpringApplication.run(IrsDemoWebApplication::class.java, *args) SpringApplication.run(IrsDemoWebApplication::class.java, *args)
} }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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