Merge pull request #834 from corda/clint-irswebfix

IRS web demo now shows fixings + general IRS fixes
This commit is contained in:
Clinton 2017-06-15 17:49:43 +01:00 committed by GitHub
commit a6853be035
15 changed files with 167 additions and 52 deletions

View File

@ -23,6 +23,7 @@ import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal
import java.security.PublicKey
import java.time.LocalDate
import java.util.*
/**
@ -80,6 +81,7 @@ object JacksonSupport {
addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
addDeserializer(SecureHash::class.java, SecureHashDeserializer())
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
addSerializer(BusinessCalendar::class.java, CalendarSerializer)
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys
@ -268,11 +270,30 @@ object JacksonSupport {
}
}
data class BusinessCalendarWrapper(val holidayDates: List<LocalDate>) {
fun toCalendar() = BusinessCalendar(holidayDates)
}
object CalendarSerializer : JsonSerializer<BusinessCalendar>() {
override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) {
val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj }
if(calendarName != null) {
generator.writeString(calendarName)
} else {
generator.writeObject(BusinessCalendarWrapper(obj.holidayDates))
}
}
}
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
return try {
val array = StringArrayDeserializer.instance.deserialize(parser, context)
BusinessCalendar.getInstance(*array)
try {
val array = StringArrayDeserializer.instance.deserialize(parser, context)
BusinessCalendar.getInstance(*array)
} catch (e: Exception) {
parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar()
}
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}")
}

View File

@ -633,7 +633,7 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc
* no staff are around to handle problems.
*/
@CordaSerializable
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) {
open class BusinessCalendar(val holidayDates: List<LocalDate>) {
@CordaSerializable
class UnknownCalendar(name: String) : Exception("$name not found")

View File

@ -4,7 +4,9 @@ import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.services.ServiceInfo
import net.corda.core.toFuture
import net.corda.core.utilities.DUMMY_BANK_A
import net.corda.core.utilities.DUMMY_BANK_B
import net.corda.core.utilities.DUMMY_NOTARY
@ -22,14 +24,18 @@ import net.corda.testing.http.HttpApi
import org.apache.commons.io.IOUtils
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import rx.Observable
import rx.observables.BlockingObservable
import java.net.URL
import java.time.Duration
import java.time.LocalDate
import java.time.temporal.ChronoUnit
class IRSDemoTest : IntegrationTestCategory {
val rpcUser = User("user", "password", emptySet())
val currentDate: LocalDate = LocalDate.now()
val futureDate: LocalDate = currentDate.plusMonths(6)
val maxWaitTime: Duration = Duration.of(60, ChronoUnit.SECONDS)
@Test
fun `runs IRS demo`() {
@ -40,56 +46,67 @@ class IRSDemoTest : IntegrationTestCategory {
startNode(DUMMY_BANK_B.name)
).getOrThrow()
println("All nodes started")
val (controllerAddr, nodeAAddr, nodeBAddr) = Futures.allAsList(
startWebserver(controller),
startWebserver(nodeA),
startWebserver(nodeB)
).getOrThrow().map { it.listenAddress }
println("All webservers started")
val (controllerApi, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map {
val mapper = net.corda.jackson.JacksonSupport.createDefaultMapper(it.first.rpc)
HttpApi.fromHostAndPort(it.second, "api/irs", mapper = mapper)
}
val nextFixingDates = getFixingDateObservable(nodeA.configuration)
val numADeals = getTradeCount(nodeAAddr)
val numBDeals = getTradeCount(nodeBAddr)
val numADeals = getTradeCount(nodeAApi)
val numBDeals = getTradeCount(nodeBApi)
runUploadRates(controllerAddr)
runTrade(nodeAAddr)
runTrade(nodeAApi)
assertThat(getTradeCount(nodeAAddr)).isEqualTo(numADeals + 1)
assertThat(getTradeCount(nodeBAddr)).isEqualTo(numBDeals + 1)
assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1)
assertThat(getTradeCount(nodeBApi)).isEqualTo(numBDeals + 1)
assertThat(getFloatingLegFixCount(nodeAApi) == 0)
// Wait until the initial trade and all scheduled fixings up to the current date have finished
nextFixingDates.first { it == null || it > currentDate }
nextFixingDates.firstWithTimeout(maxWaitTime){ it == null || it > currentDate }
runDateChange(nodeBApi)
nextFixingDates.firstWithTimeout(maxWaitTime) { it == null || it > futureDate }
runDateChange(nodeBAddr)
nextFixingDates.first { it == null || it > futureDate }
assertThat(getFloatingLegFixCount(nodeAApi) > 0)
}
}
fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable<LocalDate?> {
fun getFloatingLegFixCount(nodeApi: HttpApi) = getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null }
fun getFixingDateObservable(config: FullNodeConfiguration): Observable<LocalDate?> {
val client = CordaRPCClient(config.rpcAddress!!)
val proxy = client.start("user", "password").proxy
val vaultUpdates = proxy.vaultAndUpdates().second
val fixingDates = vaultUpdates.map { update ->
return vaultUpdates.map { update ->
val irsStates = update.produced.map { it.state.data }.filterIsInstance<InterestRateSwap.State>()
irsStates.mapNotNull { it.calculation.nextFixingDate() }.max()
}.cache().toBlocking()
return fixingDates
}.cache()
}
private fun runDateChange(nodeAddr: HostAndPort) {
val url = URL("http://$nodeAddr/api/irs/demodate")
assertThat(putJson(url, "\"$futureDate\"")).isTrue()
private fun runDateChange(nodeApi: HttpApi) {
println("Running date change against ${nodeApi.root}")
assertThat(nodeApi.putJson("demodate", "\"$futureDate\"")).isTrue()
}
private fun runTrade(nodeAddr: HostAndPort) {
private fun runTrade(nodeApi: HttpApi) {
println("Running trade against ${nodeApi.root}")
val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json")
val tradeFile = fileContents.replace("tradeXXX", "trade1")
val url = URL("http://$nodeAddr/api/irs/deals")
assertThat(postJson(url, tradeFile)).isTrue()
assertThat(nodeApi.postJson("deals", tradeFile)).isTrue()
}
private fun runUploadRates(host: HostAndPort) {
println("Running upload rates against $host")
val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt")
val url = URL("http://$host/upload/interest-rates")
assertThat(uploadFile(url, fileContents)).isTrue()
@ -99,9 +116,19 @@ class IRSDemoTest : IntegrationTestCategory {
return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name())
}
private fun getTradeCount(nodeAddr: HostAndPort): Int {
val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs")
val deals = api.getJson<Array<*>>("deals")
private fun getTradeCount(nodeApi: HttpApi): Int {
println("Getting trade count from ${nodeApi.root}")
val deals = nodeApi.getJson<Array<*>>("deals")
return deals.size
}
private fun getTrades(nodeApi: HttpApi): Array<InterestRateSwap.State> {
println("Getting trades from ${nodeApi.root}")
val deals = nodeApi.getJson<Array<InterestRateSwap.State>>("deals")
return deals
}
fun<T> Observable<T>.firstWithTimeout(timeout: Duration, pred: (T) -> Boolean) {
first(pred).toFuture().getOrThrow(timeout)
}
}

View File

@ -1,5 +1,8 @@
package net.corda.irs.contract
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import net.corda.core.contracts.*
import net.corda.core.contracts.clauses.*
import net.corda.core.crypto.SecureHash
@ -106,6 +109,7 @@ abstract class RatePaymentEvent(date: LocalDate,
* Assumes that the rate is valid.
*/
@CordaSerializable
@JsonIgnoreProperties(ignoreUnknown = true)
class FixedRatePaymentEvent(date: LocalDate,
accrualStartDate: LocalDate,
accrualEndDate: LocalDate,
@ -129,6 +133,7 @@ class FixedRatePaymentEvent(date: LocalDate,
* If the rate is null returns a zero payment. // TODO: Is this the desired behaviour?
*/
@CordaSerializable
@JsonIgnoreProperties(ignoreUnknown = true)
class FloatingRatePaymentEvent(date: LocalDate,
accrualStartDate: LocalDate,
accrualEndDate: LocalDate,
@ -651,6 +656,7 @@ class InterestRateSwap : Contract {
/**
* The state class contains the 4 major data classes.
*/
@JsonIgnoreProperties("parties", "participants", ignoreUnknown = true)
data class State(
val fixedLeg: FixedLeg,
val floatingLeg: FloatingLeg,

View File

@ -1,5 +1,7 @@
package net.corda.irs.contract
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import net.corda.core.contracts.Amount
import net.corda.core.contracts.Tenor
import net.corda.core.serialization.CordaSerializable
@ -36,6 +38,7 @@ val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this)
/**
* Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
open class Rate(val ratioUnit: RatioUnit? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -63,6 +66,7 @@ open class Rate(val ratioUnit: RatioUnit? = null) {
*/
@CordaSerializable
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
@JsonIgnore
fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0")
override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other)

View File

@ -6,10 +6,12 @@ define([
'utils/semantic',
'utils/dayCountBasisLookup',
'services/NodeApi',
'Deal'
'Deal',
'services/HttpErrorHandler'
], (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) => {
angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService) {
angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService, httpErrorHandler) {
semantic.init($scope, nodeService.isLoading);
let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
$scope.dayCountBasisLookup = dayCountBasisLookup;
$scope.deal = nodeService.newDeal();
@ -17,7 +19,7 @@ define([
nodeService.createDeal(new Deal($scope.deal))
.then((tradeId) => $location.path('#/deal/' + tradeId), (resp) => {
$scope.formError = resp.data;
});
}, handleHttpFail);
};
$('input.percent').mask("9.999999%", {placeholder: "", autoclear: false});
$('#swapirscolumns').click(() => {

View File

@ -1,9 +1,19 @@
'use strict';
define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => {
angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService) {
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (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) + "%": "";
});
nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = deal);
return deal;
};
nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = decorateDeal(deal), handleHttpFail);
});
});

View File

@ -1,12 +1,10 @@
'use strict';
define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => {
angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService) {
define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (angular, semantic) => {
angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService, httpErrorHandler) {
semantic.addLoadingModal($scope, nodeService.isLoading);
let handleHttpFail = (resp) => {
$scope.httpError = resp.data
};
let handleHttpFail = httpErrorHandler.createErrorHandler($scope);
$scope.infoMsg = "";
$scope.errorText = "";
@ -28,7 +26,7 @@ define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, no
return name;
};
nodeService.getDate().then((date) => $scope.date = date);
nodeService.getDeals().then((deals) => $scope.deals = deals);
nodeService.getDate().then((date) => $scope.date = date, handleHttpFail);
nodeService.getDeals().then((deals) => $scope.deals = deals, handleHttpFail);
});
});

View File

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

View File

@ -5,6 +5,7 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
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
let load = (type, promise) => {
curLoading[type] = true;
@ -17,19 +18,20 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
});
};
let endpoint = (target) => serverAddr + target;
let changeDateOnNode = (newDate) => {
const dateStr = formatDateForNode(newDate);
let endpoint = '/api/irs/demodate';
return load('date', $http.put(endpoint, "\"" + dateStr + "\"")).then((resp) => {
return load('date', $http.put(endpoint('/api/irs/demodate'), "\"" + dateStr + "\"")).then((resp) => {
date = newDate;
return this.getDateModel(date);
});
};
this.getDate = () => {
return load('date', $http.get('/api/irs/demodate')).then((resp) => {
const parts = resp.data.split("-");
date = new Date(parts[0], parts[1] - 1, parts[2]); // JS uses 0 based months
return load('date', $http.get(endpoint('/api/irs/demodate'))).then((resp) => {
const dateParts = resp.data;
date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); // JS uses 0 based months
return this.getDateModel(date);
});
};
@ -54,13 +56,13 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
};
this.getDeals = () => {
return load('deals', $http.get('/api/irs/deals')).then((resp) => {
return load('deals', $http.get(endpoint('/api/irs/deals'))).then((resp) => {
return resp.data.reverse();
});
};
this.getDeal = (dealId) => {
return load('deal' + dealId, $http.get('/api/irs/deals/' + dealId)).then((resp) => {
return load('deal' + dealId, $http.get(endpoint('/api/irs/deals/' + dealId))).then((resp) => {
// Do some data modification to simplify the model
let deal = resp.data;
deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.ratioUnit.value * 100).toString().slice(0, 6);
@ -87,7 +89,7 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => {
};
this.createDeal = (deal) => {
return load('create-deal', $http.post('/api/irs/deals', deal.toJson()))
return load('create-deal', $http.post(endpoint('/api/irs/deals'), deal.toJson()))
.then((resp) => {
return deal.tradeId;
}, (resp) => {

View File

@ -1,5 +1,7 @@
<div class="ui container">
<div class="ui hidden divider"></div>
<div class="ui negative message" id="form-error" ng-show="formError">{{formError}}</div>
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
<h3 class="ui horizontal divider header">
<i class="list icon"></i>
New Deal

View File

@ -1,5 +1,6 @@
<div class="ui container">
<div class="ui hidden divider"></div>
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
<div class="ui grid">
<div class="sixteen wide column" id="common">
<table class="ui striped table">
@ -199,6 +200,26 @@
</div>
</td>
</tr>
<tr class="center aligned">
<td colspan="2">
<div class="ui accordion">
<div class="title">
<i class="dropdown icon"></i>
Fixings
</div>
<div class="content">
<table class="ui celled small table">
<tbody>
<tr class="center aligned" ng-repeat="fixing in deal.calculation.floatingLegPaymentSchedule">
<td>{{fixing.fixingDate[0]}}-{{fixing.fixingDate[1]}}-{{fixing.fixingDate[2]}}</td>
<td>{{fixing.ratePercent}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,4 +1,5 @@
<div class="ui container">
<div class="ui hidden divider"></div>
<div class="ui negative message" id="http-error" ng-show="httpError">{{httpError}}</div>
<div class="ui info message" id="info-message" ng-show="infoMsg">{{infoMsg}}</div>
<div class="ui active dimmer" ng-show="isLoading()">

View File

@ -1,9 +1,10 @@
package net.corda.testing.http
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.net.HostAndPort
import java.net.URL
class HttpApi(val root: URL) {
class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) {
/**
* Send a PUT with a payload to the path on the API specified.
*
@ -21,12 +22,15 @@ class HttpApi(val root: URL) {
/**
* Send a GET request to the path on the API specified.
*/
inline fun <reified T : Any> getJson(path: String, params: Map<String, String> = mapOf()) = HttpUtils.getJson<T>(URL(root, path), params)
inline fun <reified T : Any> getJson(path: String, params: Map<String, String> = mapOf()) = HttpUtils.getJson<T>(URL(root, path), params, mapper)
private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any)
companion object {
fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi
= HttpApi(URL("$protocol://$hostAndPort/$base/"))
fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi
= HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper)
private val defaultMapper: ObjectMapper by lazy {
net.corda.jackson.JacksonSupport.createNonRpcMapper()
}
}
}

View File

@ -35,10 +35,10 @@ object HttpUtils {
return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build())
}
inline fun <reified T : Any> getJson(url: URL, params: Map<String, String> = mapOf()): T {
inline fun <reified T : Any> getJson(url: URL, params: Map<String, String> = mapOf(), mapper: ObjectMapper = defaultMapper): T {
val paramString = if (params.isEmpty()) "" else "?" + params.map { "${it.key}=${it.value}" }.joinToString("&")
val parameterisedUrl = URL(url.toExternalForm() + paramString)
return defaultMapper.readValue(parameterisedUrl, T::class.java)
return mapper.readValue(parameterisedUrl, T::class.java)
}
private fun makeRequest(request: Request): Boolean {