mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Merged in plt-17-api-server-merge5 (pull request #51)
IRS related protocols and scripts
This commit is contained in:
commit
34940c903b
@ -144,6 +144,14 @@ task getRateFixDemo(type: CreateStartScripts) {
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
task getIRSDemo(type: CreateStartScripts) {
|
||||
mainClassName = "demos.IRSDemoKt"
|
||||
applicationName = "irsdemo"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
// These lines tell gradle to run the Quasar suspendables scanner to look for unannotated super methods
|
||||
// that have @Suspendable sub implementations. These tend to cause NPEs and are not caught by the verifier
|
||||
// NOTE: need to make sure the output isn't on the classpath or every other run it generates empty results, so
|
||||
@ -166,5 +174,6 @@ jar.dependsOn quasarScan
|
||||
|
||||
applicationDistribution.into("bin") {
|
||||
from(getRateFixDemo)
|
||||
from(getIRSDemo)
|
||||
fileMode = 0755
|
||||
}
|
@ -8,18 +8,8 @@
|
||||
|
||||
package contracts
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonSerializer
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.type.SimpleType
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.node.services.DummyTimestampingAuthority
|
||||
import org.apache.commons.jexl3.JexlBuilder
|
||||
import org.apache.commons.jexl3.MapContext
|
||||
import java.math.BigDecimal
|
||||
@ -31,7 +21,16 @@ import java.util.*
|
||||
val IRS_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash-of-irs-code")
|
||||
|
||||
// This is a placeholder for some types that we haven't identified exactly what they are just yet for things still in discussion
|
||||
open class UnknownType()
|
||||
open class UnknownType() {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (other is UnknownType)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event superclass - everything happens on a date.
|
||||
@ -182,7 +181,7 @@ class InterestRateSwap() : Contract {
|
||||
* data that will changed from state to state (Recall that the design insists that everything is immutable, so we actually
|
||||
* copy / update for each transition)
|
||||
*/
|
||||
data class Calculation(
|
||||
data class Calculation (
|
||||
val expression: Expression,
|
||||
val floatingLegPaymentSchedule: Map<LocalDate, FloatingRatePaymentEvent>,
|
||||
val fixedLegPaymentSchedule: Map<LocalDate, FixedRatePaymentEvent>
|
||||
@ -240,6 +239,48 @@ class InterestRateSwap() : Contract {
|
||||
"TerminationDateAdjustment=$terminationDateAdjustment,DayCountBasis=$dayCountBasisDay/$dayCountBasisYear,DayInMonth=$dayInMonth," +
|
||||
"PaymentRule=$paymentRule,PaymentDelay=$paymentDelay,PaymentCalendar=$paymentCalendar,InterestPeriodAdjustment=$interestPeriodAdjustment"
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as CommonLeg
|
||||
|
||||
if (notional != other.notional) return false
|
||||
if (paymentFrequency != other.paymentFrequency) return false
|
||||
if (effectiveDate != other.effectiveDate) return false
|
||||
if (effectiveDateAdjustment != other.effectiveDateAdjustment) return false
|
||||
if (terminationDate != other.terminationDate) return false
|
||||
if (terminationDateAdjustment != other.terminationDateAdjustment) return false
|
||||
if (dayCountBasisDay != other.dayCountBasisDay) return false
|
||||
if (dayCountBasisYear != other.dayCountBasisYear) return false
|
||||
if (dayInMonth != other.dayInMonth) return false
|
||||
if (paymentRule != other.paymentRule) return false
|
||||
if (paymentDelay != other.paymentDelay) return false
|
||||
if (paymentCalendar != other.paymentCalendar) return false
|
||||
if (interestPeriodAdjustment != other.interestPeriodAdjustment) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int{
|
||||
var result = notional.hashCode()
|
||||
result += 31 * result + paymentFrequency.hashCode()
|
||||
result += 31 * result + effectiveDate.hashCode()
|
||||
result += 31 * result + (effectiveDateAdjustment?.hashCode() ?: 0)
|
||||
result += 31 * result + terminationDate.hashCode()
|
||||
result += 31 * result + (terminationDateAdjustment?.hashCode() ?: 0)
|
||||
result += 31 * result + dayCountBasisDay.hashCode()
|
||||
result += 31 * result + dayCountBasisYear.hashCode()
|
||||
result += 31 * result + dayInMonth
|
||||
result += 31 * result + paymentRule.hashCode()
|
||||
result += 31 * result + paymentDelay
|
||||
result += 31 * result + paymentCalendar.hashCode()
|
||||
result += 31 * result + interestPeriodAdjustment.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
open class FixedLeg(
|
||||
@ -264,6 +305,29 @@ class InterestRateSwap() : Contract {
|
||||
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) {
|
||||
override fun toString(): String = "FixedLeg(Payer=$fixedRatePayer," + super.toString() + ",fixedRate=$fixedRate," +
|
||||
"rollConvention=$rollConvention"
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FixedLeg
|
||||
|
||||
if (fixedRatePayer != other.fixedRatePayer) return false
|
||||
if (fixedRate != other.fixedRate) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int{
|
||||
var result = super.hashCode()
|
||||
result += 31 * result + fixedRatePayer.hashCode()
|
||||
result += 31 * result + fixedRate.hashCode()
|
||||
result += 31 * result + rollConvention.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open class FloatingLeg(
|
||||
@ -298,6 +362,44 @@ class InterestRateSwap() : Contract {
|
||||
"FixingPeriond=$fixingPeriod,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," +
|
||||
"Index=$index,IndexSource=$indexSource,IndexTenor=$indexTenor"
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
if (!super.equals(other)) return false
|
||||
|
||||
other as FloatingLeg
|
||||
|
||||
if (floatingRatePayer != other.floatingRatePayer) return false
|
||||
if (rollConvention != other.rollConvention) return false
|
||||
if (fixingRollConvention != other.fixingRollConvention) return false
|
||||
if (resetDayInMonth != other.resetDayInMonth) return false
|
||||
if (fixingPeriod != other.fixingPeriod) return false
|
||||
if (resetRule != other.resetRule) return false
|
||||
if (fixingsPerPayment != other.fixingsPerPayment) return false
|
||||
if (fixingCalendar != other.fixingCalendar) return false
|
||||
if (index != other.index) return false
|
||||
if (indexSource != other.indexSource) return false
|
||||
if (indexTenor != other.indexTenor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int{
|
||||
var result = super.hashCode()
|
||||
result += 31 * result + floatingRatePayer.hashCode()
|
||||
result += 31 * result + rollConvention.hashCode()
|
||||
result += 31 * result + fixingRollConvention.hashCode()
|
||||
result += 31 * result + resetDayInMonth
|
||||
result += 31 * result + fixingPeriod.hashCode()
|
||||
result += 31 * result + resetRule.hashCode()
|
||||
result += 31 * result + fixingsPerPayment.hashCode()
|
||||
result += 31 * result + fixingCalendar.hashCode()
|
||||
result += 31 * result + index.hashCode()
|
||||
result += 31 * result + indexSource.hashCode()
|
||||
result += 31 * result + indexTenor.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -305,7 +407,7 @@ class InterestRateSwap() : Contract {
|
||||
*/
|
||||
override fun verify(tx: TransactionForVerification) {
|
||||
val command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>()
|
||||
val time = tx.commands.getTimestampByName("Mock Company 0", "Bank of Zurich")?.midpoint
|
||||
val time = tx.commands.getTimestampByName("Mock Company 0", "Bank A")?.midpoint
|
||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||
|
||||
val irs = tx.outStates.filterIsInstance<InterestRateSwap.State>().single()
|
||||
@ -356,7 +458,8 @@ class InterestRateSwap() : Contract {
|
||||
val floatingLeg: FloatingLeg,
|
||||
val calculation: Calculation,
|
||||
val common: Common
|
||||
) : LinearState {
|
||||
) : FixableDealState {
|
||||
|
||||
override val programRef = IRS_PROGRAM_ID
|
||||
override val thread = SecureHash.sha256(common.tradeID)
|
||||
override val ref = common.tradeID
|
||||
@ -365,6 +468,39 @@ class InterestRateSwap() : Contract {
|
||||
return (fixedLeg.fixedRatePayer.owningKey in ourKeys) || (floatingLeg.floatingRatePayer.owningKey in ourKeys)
|
||||
}
|
||||
|
||||
override val parties: Array<Party>
|
||||
get() = arrayOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer)
|
||||
|
||||
override fun withPublicKey(before: Party, after: PublicKey): State {
|
||||
val newParty = Party(before.name, after)
|
||||
if(before == fixedLeg.fixedRatePayer) {
|
||||
val deal = copy()
|
||||
deal.fixedLeg.fixedRatePayer = newParty
|
||||
return deal
|
||||
} else if(before == floatingLeg.floatingRatePayer) {
|
||||
val deal = copy()
|
||||
deal.floatingLeg.floatingRatePayer = newParty
|
||||
return deal
|
||||
} else {
|
||||
throw IllegalArgumentException("No such party: $before")
|
||||
}
|
||||
}
|
||||
|
||||
override fun generateAgreement(): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common)
|
||||
|
||||
override fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix) {
|
||||
InterestRateSwap().generateFix(ptx, StateAndRef(this, oldStateRef), Pair(fix.of.forDay, Rate(RatioUnit(fix.value))))
|
||||
}
|
||||
|
||||
override fun nextFixingOf(): FixOf? {
|
||||
val date = calculation.nextFixingDate()
|
||||
return if (date==null) null else {
|
||||
val fixingEvent = calculation.getFixing(date)
|
||||
val oracleRate = fixingEvent.rate as ReferenceRate
|
||||
FixOf(oracleRate.name, date, oracleRate.tenor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For evaluating arbitrary java on the platform
|
||||
*/
|
||||
|
@ -1,17 +1,8 @@
|
||||
package contracts
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
|
||||
import com.fasterxml.jackson.databind.type.SimpleType
|
||||
import core.Amount
|
||||
import core.Tenor
|
||||
import core.*
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.security.PublicKey
|
||||
|
||||
|
||||
// Things in here will move to the general utils class when we've hammered out various discussions regarding amounts, dates, oracle etc.
|
||||
@ -21,6 +12,22 @@ import java.time.LocalDate
|
||||
*/
|
||||
open class RatioUnit(value: BigDecimal) { // TODO: Discuss this type
|
||||
val value = value
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as RatioUnit
|
||||
|
||||
if (value != other.value) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int{
|
||||
return value.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,10 +44,78 @@ open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecima
|
||||
*/
|
||||
val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this)
|
||||
|
||||
/**
|
||||
* Interface representing an agreement that exposes various attributes that are common and allow
|
||||
* implementation of general protocols that manipulate many agreement types
|
||||
*/
|
||||
interface DealState : LinearState {
|
||||
|
||||
/** Human readable well known reference (e.g. trade reference) */
|
||||
val ref: String
|
||||
|
||||
/** Exposes the Parties involved in a generic way */
|
||||
val parties: Array<Party>
|
||||
|
||||
/** Allow swapping in of potentially transaction specific public keys prior to signing */
|
||||
fun withPublicKey(before: Party, after: PublicKey): DealState
|
||||
|
||||
/**
|
||||
* Generate a partial transaction representing an agreement (command) to this deal, allowing a general
|
||||
* deal/agreement protocol to generate the necessary transaction for potential implementations
|
||||
*
|
||||
* TODO: Currently this is the "inception" transaction but in future an offer of some description might be an input state ref
|
||||
*
|
||||
* TODO: This should more likely be a method on the Contract (on a common interface) and the changes to reference a
|
||||
* Contract instance from a ContractState are imminent, at which point we can move this out of here
|
||||
*/
|
||||
fun generateAgreement(): TransactionBuilder
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface adding fixing specific methods
|
||||
*/
|
||||
interface FixableDealState : DealState {
|
||||
/**
|
||||
* When is the next fixing and what is the fixing for?
|
||||
*
|
||||
* TODO: In future we would use this to register for an event to trigger a/the fixing protocol
|
||||
*/
|
||||
fun nextFixingOf(): FixOf?
|
||||
|
||||
/**
|
||||
* Generate a fixing command for this deal and fix
|
||||
*
|
||||
* TODO: This would also likely move to methods on the Contract once the changes to reference
|
||||
* the Contract from the ContractState are in
|
||||
*/
|
||||
fun generateFix(ptx: TransactionBuilder, oldStateRef: StateRef, fix: Fix)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc
|
||||
*/
|
||||
open class Rate(val ratioUnit: RatioUnit? = null)
|
||||
open class Rate(val ratioUnit: RatioUnit? = null) {
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as Rate
|
||||
|
||||
if (ratioUnit != other.ratioUnit) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings
|
||||
* that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested
|
||||
* for equality.
|
||||
*/
|
||||
override fun hashCode(): Int{
|
||||
return ratioUnit?.hashCode() ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A very basic subclass to represent a fixed rate.
|
||||
|
@ -269,7 +269,7 @@ open class BusinessCalendar private constructor(val calendars: Array<out String>
|
||||
calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }.
|
||||
toSet().
|
||||
map{ parseDateFromString(it) }.
|
||||
toList()
|
||||
toList().sorted()
|
||||
)
|
||||
|
||||
/** Calculates an event schedule that moves events around to ensure they fall on working days. */
|
||||
@ -299,6 +299,17 @@ open class BusinessCalendar private constructor(val calendars: Array<out String>
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean = if (other is BusinessCalendar) {
|
||||
/** Note this comparison is OK as we ensure they are sorted in getInstance() */
|
||||
this.holidayDates == other.holidayDates
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return this.holidayDates.hashCode()
|
||||
}
|
||||
|
||||
open fun isWorkingDay(date: LocalDate): Boolean =
|
||||
when {
|
||||
date.dayOfWeek == DayOfWeek.SATURDAY -> false
|
||||
|
@ -18,6 +18,7 @@ import java.io.OutputStream
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
/** Implemented by anything that can be named by a secure hash value (e.g. transactions, attachments). */
|
||||
@ -56,10 +57,6 @@ interface LinearState: ContractState {
|
||||
/** Unique thread id within the wallets of all parties */
|
||||
val thread: SecureHash
|
||||
|
||||
/** Human readable well known reference (e.g. trade reference) */
|
||||
// TODO we will push this down out of here once we have something more sophisticated and a more powerful query API
|
||||
val ref: String
|
||||
|
||||
/** true if this should be tracked by our wallet(s) */
|
||||
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
|
||||
}
|
||||
|
104
scripts/example-irs-trade.json
Normal file
104
scripts/example-irs-trade.json
Normal file
@ -0,0 +1,104 @@
|
||||
{
|
||||
"fixedLeg": {
|
||||
"fixedRatePayer": "Bank A",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "SemiAnnual",
|
||||
"effectiveDate": "2016-03-16",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-16",
|
||||
"terminationDateAdjustment": null,
|
||||
"fixedRate": {
|
||||
"ratioUnit": {
|
||||
"value": "0.01676"
|
||||
}
|
||||
},
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": "London",
|
||||
"interestPeriodAdjustment": "Adjusted"
|
||||
},
|
||||
"floatingLeg": {
|
||||
"floatingRatePayer": "Bank B",
|
||||
"notional": {
|
||||
"pennies": 2500000000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"paymentFrequency": "Quarterly",
|
||||
"effectiveDate": "2016-03-12",
|
||||
"effectiveDateAdjustment": null,
|
||||
"terminationDate": "2026-03-12",
|
||||
"terminationDateAdjustment": null,
|
||||
"dayCountBasisDay": "D30",
|
||||
"dayCountBasisYear": "Y360",
|
||||
"rollConvention": "ModifiedFollowing",
|
||||
"fixingRollConvention": "ModifiedFollowing",
|
||||
"dayInMonth": 10,
|
||||
"resetDayInMonth": 10,
|
||||
"paymentRule": "InArrears",
|
||||
"paymentDelay": 0,
|
||||
"paymentCalendar": [ "London" ],
|
||||
"interestPeriodAdjustment": "Adjusted",
|
||||
"fixingPeriod": "TWODAYS",
|
||||
"resetRule": "InAdvance",
|
||||
"fixingsPerPayment": "Quarterly",
|
||||
"fixingCalendar": [ "NewYork" ],
|
||||
"index": "ICE LIBOR",
|
||||
"indexSource": "Rates Service Provider",
|
||||
"indexTenor": {
|
||||
"name": "3M"
|
||||
}
|
||||
},
|
||||
"calculation": {
|
||||
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
|
||||
"floatingLegPaymentSchedule": {
|
||||
},
|
||||
"fixedLegPaymentSchedule": {
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"baseCurrency": "EUR",
|
||||
"eligibleCurrency": "EUR",
|
||||
"eligibleCreditSupport": "Cash in an Eligible Currency",
|
||||
"independentAmounts": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"threshold": {
|
||||
"pennies": 0,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"minimumTransferAmount": {
|
||||
"pennies": 25000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"rounding": {
|
||||
"pennies": 1000000,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"valuationDate": "Every Local Business Day",
|
||||
"notificationTime": "2:00pm London",
|
||||
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
|
||||
"interestRate": {
|
||||
"oracle": "Rates Service Provider",
|
||||
"tenor": {
|
||||
"name": "6M"
|
||||
},
|
||||
"ratioUnit": null,
|
||||
"name": "EONIA"
|
||||
},
|
||||
"addressForTransfers": "",
|
||||
"exposure": {},
|
||||
"localBusinessDay": [ "London" , "NewYork" ],
|
||||
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360",
|
||||
"tradeID": "tradeXXX",
|
||||
"hashLegalDocs": "put hash here"
|
||||
},
|
||||
"programRef": "1E6BBA305D445341F0026E51B6C7F3ACB834AFC6C2510C0EF7BC0477235EFECF"
|
||||
}
|
@ -1,51 +1,51 @@
|
||||
# Some pretend noddy rate fixes, for the interest rate oracles.
|
||||
|
||||
LIBOR 2016-03-16 1M = 0.678
|
||||
LIBOR 2016-03-16 2M = 0.655
|
||||
ICE LIBOR 2016-03-16 1M = 0.678
|
||||
ICE LIBOR 2016-03-16 2M = 0.655
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
|
||||
LIBOR 2016-03-08 3M = 0.01
|
||||
LIBOR 2016-06-08 3M = 0.01
|
||||
LIBOR 2016-09-08 3M = 0.01
|
||||
LIBOR 2016-12-08 3M = 0.01
|
||||
LIBOR 2017-03-08 3M = 0.01
|
||||
LIBOR 2017-06-08 3M = 0.01
|
||||
LIBOR 2017-09-07 3M = 0.01
|
||||
LIBOR 2017-12-07 3M = 0.01
|
||||
LIBOR 2018-03-08 3M = 0.01
|
||||
LIBOR 2018-06-07 3M = 0.01
|
||||
LIBOR 2018-09-06 3M = 0.01
|
||||
LIBOR 2018-12-06 3M = 0.01
|
||||
LIBOR 2019-03-07 3M = 0.01
|
||||
LIBOR 2019-06-06 3M = 0.01
|
||||
LIBOR 2019-09-06 3M = 0.01
|
||||
LIBOR 2019-12-06 3M = 0.01
|
||||
LIBOR 2020-03-06 3M = 0.01
|
||||
LIBOR 2020-06-08 3M = 0.01
|
||||
LIBOR 2020-09-08 3M = 0.01
|
||||
LIBOR 2020-12-08 3M = 0.01
|
||||
LIBOR 2021-03-08 3M = 0.01
|
||||
LIBOR 2021-06-08 3M = 0.01
|
||||
LIBOR 2021-09-08 3M = 0.01
|
||||
LIBOR 2021-12-08 3M = 0.01
|
||||
LIBOR 2022-03-08 3M = 0.01
|
||||
LIBOR 2022-06-08 3M = 0.01
|
||||
LIBOR 2022-09-08 3M = 0.01
|
||||
LIBOR 2022-12-08 3M = 0.01
|
||||
LIBOR 2023-03-08 3M = 0.01
|
||||
LIBOR 2023-06-08 3M = 0.01
|
||||
LIBOR 2023-09-07 3M = 0.01
|
||||
LIBOR 2023-12-07 3M = 0.01
|
||||
LIBOR 2024-03-07 3M = 0.01
|
||||
LIBOR 2024-06-06 3M = 0.01
|
||||
LIBOR 2024-09-06 3M = 0.01
|
||||
LIBOR 2024-12-06 3M = 0.01
|
||||
LIBOR 2025-03-06 3M = 0.01
|
||||
LIBOR 2025-06-06 3M = 0.01
|
||||
LIBOR 2025-09-08 3M = 0.01
|
||||
LIBOR 2025-12-08 3M = 0.01
|
||||
LIBOR 2026-03-06 3M = 0.01
|
||||
LIBOR 2026-06-08 3M = 0.01
|
||||
LIBOR 2026-09-08 3M = 0.01
|
||||
LIBOR 2026-12-08 3M = 0.01
|
||||
ICE LIBOR 2016-03-08 3M = 0.0063515
|
||||
ICE LIBOR 2016-06-08 3M = 0.0063520
|
||||
ICE LIBOR 2016-09-08 3M = 0.0063521
|
||||
ICE LIBOR 2016-12-08 3M = 0.0063515
|
||||
ICE LIBOR 2017-03-08 3M = 0.0063525
|
||||
ICE LIBOR 2017-06-08 3M = 0.0063530
|
||||
ICE LIBOR 2017-09-07 3M = 0.0063531
|
||||
ICE LIBOR 2017-12-07 3M = 0.0063532
|
||||
ICE LIBOR 2018-03-08 3M = 0.0063533
|
||||
ICE LIBOR 2018-06-07 3M = 0.0063534
|
||||
ICE LIBOR 2018-09-06 3M = 0.0063535
|
||||
ICE LIBOR 2018-12-06 3M = 0.0063536
|
||||
ICE LIBOR 2019-03-07 3M = 0.0063537
|
||||
ICE LIBOR 2019-06-06 3M = 0.0063538
|
||||
ICE LIBOR 2019-09-06 3M = 0.0063539
|
||||
ICE LIBOR 2019-12-06 3M = 0.0063540
|
||||
ICE LIBOR 2020-03-06 3M = 0.0063541
|
||||
ICE LIBOR 2020-06-08 3M = 0.0063542
|
||||
ICE LIBOR 2020-09-08 3M = 0.0063543
|
||||
ICE LIBOR 2020-12-08 3M = 0.0063544
|
||||
ICE LIBOR 2021-03-08 3M = 0.0063545
|
||||
ICE LIBOR 2021-06-08 3M = 0.0063546
|
||||
ICE LIBOR 2021-09-08 3M = 0.0063547
|
||||
ICE LIBOR 2021-12-08 3M = 0.0063548
|
||||
ICE LIBOR 2022-03-08 3M = 0.0063549
|
||||
ICE LIBOR 2022-06-08 3M = 0.0063550
|
||||
ICE LIBOR 2022-09-08 3M = 0.0063551
|
||||
ICE LIBOR 2022-12-08 3M = 0.0063553
|
||||
ICE LIBOR 2023-03-08 3M = 0.0063554
|
||||
ICE LIBOR 2023-06-08 3M = 0.0063555
|
||||
ICE LIBOR 2023-09-07 3M = 0.0063556
|
||||
ICE LIBOR 2023-12-07 3M = 0.0063557
|
||||
ICE LIBOR 2024-03-07 3M = 0.0063558
|
||||
ICE LIBOR 2024-06-06 3M = 0.0063559
|
||||
ICE LIBOR 2024-09-06 3M = 0.0063560
|
||||
ICE LIBOR 2024-12-06 3M = 0.0063561
|
||||
ICE LIBOR 2025-03-06 3M = 0.0063562
|
||||
ICE LIBOR 2025-06-06 3M = 0.0063563
|
||||
ICE LIBOR 2025-09-08 3M = 0.0063564
|
||||
ICE LIBOR 2025-12-08 3M = 0.0063565
|
||||
ICE LIBOR 2026-03-06 3M = 0.0063566
|
||||
ICE LIBOR 2026-06-08 3M = 0.0063567
|
||||
ICE LIBOR 2026-09-08 3M = 0.0063568
|
||||
ICE LIBOR 2026-12-08 3M = 0.0063569
|
||||
|
65
scripts/irs-demo.sh
Executable file
65
scripts/irs-demo.sh
Executable file
@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
mode=$1
|
||||
|
||||
if [ ! -e ./gradlew ]; then
|
||||
echo "Run from the root directory please"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d build/install/r3prototyping ]; then
|
||||
./gradlew installDist
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "nodeA" ]]; then
|
||||
if [ ! -d nodeA ]; then
|
||||
mkdir nodeA
|
||||
echo "myLegalName = Bank A" >nodeA/config
|
||||
fi
|
||||
|
||||
RC=83
|
||||
while [ $RC -eq 83 ]
|
||||
do
|
||||
build/install/r3prototyping/bin/irsdemo --dir=nodeA --network-address=localhost --fake-trade-with-address=localhost:31340 --fake-trade-with-identity=nodeB/identity-public --timestamper-identity-file=nodeA/identity-public --timestamper-address=localhost --rates-oracle-address=localhost:31340 --rates-oracle-identity-file=nodeB/identity-public
|
||||
RC=$?
|
||||
done
|
||||
elif [[ "$mode" == "nodeB" ]]; then
|
||||
if [ ! -d nodeB ]; then
|
||||
mkdir nodeB
|
||||
echo "myLegalName = Bank B" >nodeB/config
|
||||
fi
|
||||
|
||||
# enable job control
|
||||
set -o monitor
|
||||
|
||||
RC=83
|
||||
while [ $RC -eq 83 ]
|
||||
do
|
||||
build/install/r3prototyping/bin/irsdemo --dir=nodeB --network-address=localhost:31340 --fake-trade-with-address=localhost --fake-trade-with-identity=nodeA/identity-public --timestamper-identity-file=nodeA/identity-public --timestamper-address=localhost --rates-oracle-address=localhost:31340 --rates-oracle-identity-file=nodeB/identity-public &
|
||||
while ! curl -F rates=@scripts/example.rates.txt http://localhost:31341/upload/interest-rates; do
|
||||
echo "Retry to upload interest rates to oracle after 5 seconds"
|
||||
sleep 5
|
||||
done
|
||||
fg %1
|
||||
RC=$?
|
||||
done
|
||||
elif [[ "$mode" == "trade" && "$2" != "" ]]; then
|
||||
tradeID=$2
|
||||
echo "Uploading tradeID ${tradeID}"
|
||||
sed "s/tradeXXX/${tradeID}/g" scripts/example-irs-trade.json | curl -H "Content-Type: application/json" -d @- http://localhost:31338/api/irs/deals
|
||||
elif [[ "$mode" == "date" && "$2" != "" ]]; then
|
||||
demodate=$2
|
||||
echo "Setting demo date to ${demodate}"
|
||||
echo "\"$demodate\"" | curl -H "Content-Type: application/json" -X PUT -d @- http://localhost:31338/api/irs/demodate
|
||||
else
|
||||
echo "Run like this, one in each tab:"
|
||||
echo
|
||||
echo " scripts/irs-demo.sh nodeA"
|
||||
echo " scripts/irs-demo.sh nodeB"
|
||||
echo
|
||||
echo "To upload a trade as e.g. trade10"
|
||||
echo " scripts/irs-demo.sh trade trade10"
|
||||
echo
|
||||
echo "To set the demo date, and post fixings in the interval, to e.g. 2017-01-30"
|
||||
echo " scripts/irs-demo.sh date 2017-01-30"
|
||||
fi
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import contracts.DealState
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.SecureHash
|
||||
@ -27,7 +28,7 @@ class APIServerImpl(val node: AbstractNode): APIServer {
|
||||
return states.values.map { it.ref }
|
||||
}
|
||||
else if (query.criteria is StatesQuery.Criteria.Deal) {
|
||||
val states = node.services.walletService.linearHeadsInstanceOf(LinearState::class.java) {
|
||||
val states = node.services.walletService.linearHeadsInstanceOf(DealState::class.java) {
|
||||
it.ref == query.criteria.ref
|
||||
}
|
||||
return states.values.map { it.ref }
|
||||
@ -73,6 +74,7 @@ class APIServerImpl(val node: AbstractNode): APIServer {
|
||||
} else if (args.containsKey(parameter.name)) {
|
||||
val value = args[parameter.name]
|
||||
if (value is Any) {
|
||||
// TODO consider supporting more complex test here to support coercing numeric/Kotlin types
|
||||
if (!(parameter.type.javaType as Class<*>).isAssignableFrom(value.javaClass)) {
|
||||
// Not null and not assignable
|
||||
break@nextConstructor
|
||||
|
109
src/main/kotlin/api/InterestRateSwapAPI.kt
Normal file
109
src/main/kotlin/api/InterestRateSwapAPI.kt
Normal file
@ -0,0 +1,109 @@
|
||||
package api
|
||||
|
||||
import contracts.InterestRateSwap
|
||||
import core.utilities.loggerFor
|
||||
import demos.protocols.AutoOfferProtocol
|
||||
import demos.protocols.ExitServerProtocol
|
||||
import demos.protocols.UpdateBusinessDayProtocol
|
||||
import java.net.URI
|
||||
import java.time.LocalDate
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
|
||||
/**
|
||||
* This provides a simplified API, currently for demonstration use only.
|
||||
*
|
||||
* It provides several JSON REST calls as follows:
|
||||
*
|
||||
* GET /api/irs/deals - returns an array of all deals tracked by the wallet of this node.
|
||||
* GET /api/irs/deals/{ref} - return the deal referenced by the externally provided refence that was previously uploaded.
|
||||
* POST /api/irs/deals - Payload is a JSON formatted [InterestRateSwap.State] create a new deal (includes an externally provided reference for use above).
|
||||
*
|
||||
* TODO: where we currently refer to singular external deal reference, of course this could easily be multiple identifiers e.g. CUSIP, ISIN.
|
||||
*
|
||||
* GET /api/irs/demodate - return the current date as viewed by the system in YYYY-MM-DD format.
|
||||
* PUT /api/irs/demodate - put date in format YYYY-MM-DD to advance the current date as viewed by the system and
|
||||
* simulate any associated business processing (currently fixing).
|
||||
*
|
||||
* TODO: replace simulated date advancement with business event based implementation
|
||||
*
|
||||
* PUT /api/irs/restart - (empty payload) cause the node to restart for API user emergency use in case any servers become unresponsive,
|
||||
* or if the demodate or population of deals should be reset (will only work while persistence is disabled).
|
||||
*/
|
||||
@Path("irs")
|
||||
class InterestRateSwapAPI(val api: APIServer) {
|
||||
|
||||
private val logger = loggerFor<InterestRateSwapAPI>()
|
||||
|
||||
private fun generateDealLink(deal: InterestRateSwap.State) = "/api/irs/deals/"+deal.common.tradeID
|
||||
|
||||
private fun getDealByRef(ref: String): InterestRateSwap.State? {
|
||||
val states = api.queryStates(StatesQuery.selectDeal(ref))
|
||||
return if (states.isEmpty()) null else {
|
||||
val deals = api.fetchStates(states).values.map { it as InterestRateSwap.State}.filterNotNull()
|
||||
return if(deals.isEmpty()) null else deals[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllDeals(): Array<InterestRateSwap.State> {
|
||||
val states = api.queryStates(StatesQuery.selectAllDeals())
|
||||
val swaps = api.fetchStates(states).values.map { it as InterestRateSwap.State }.filterNotNull().toTypedArray()
|
||||
return swaps
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("deals")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun fetchDeals(): Array<InterestRateSwap.State> = getAllDeals()
|
||||
|
||||
@POST
|
||||
@Path("deals")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun storeDeal(newDeal: InterestRateSwap.State): Response {
|
||||
api.invokeProtocolSync(ProtocolClassRef(AutoOfferProtocol.Requester::class.java.name!!), mapOf("dealToBeOffered" to newDeal))
|
||||
return Response.created(URI.create(generateDealLink(newDeal))).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("deals/{ref}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun fetchDeal(@PathParam("ref") ref: String): Response {
|
||||
val deal = getDealByRef(ref)
|
||||
if (deal == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build()
|
||||
} else {
|
||||
return Response.ok().entity(deal).build()
|
||||
}
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("demodate")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun storeDemoDate(newDemoDate: LocalDate): Response {
|
||||
val priorDemoDate = api.serverTime().toLocalDate()
|
||||
// Can only move date forwards
|
||||
if(newDemoDate.isAfter(priorDemoDate)) {
|
||||
api.invokeProtocolSync(ProtocolClassRef(UpdateBusinessDayProtocol.Broadcast::class.java.name!!), mapOf("date" to newDemoDate))
|
||||
return Response.ok().build()
|
||||
}
|
||||
val msg = "demodate is already $priorDemoDate and can only be updated with a later date"
|
||||
logger.info("Attempt to set demodate to $newDemoDate but $msg")
|
||||
return Response.status(Response.Status.CONFLICT).entity(msg).build()
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("demodate")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
fun fetchDemoDate(): LocalDate {
|
||||
return api.serverTime().toLocalDate()
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("restart")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun exitServer(): Response {
|
||||
api.invokeProtocolSync(ProtocolClassRef(ExitServerProtocol.Broadcast::class.java.name!!), mapOf("exitCode" to 83))
|
||||
return Response.ok().build()
|
||||
}
|
||||
}
|
@ -9,12 +9,13 @@
|
||||
package core.node
|
||||
|
||||
import api.Config
|
||||
import api.InterestRateSwapAPI
|
||||
import api.ResponseFilter
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import com.google.common.net.HostAndPort
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.ArtemisMessagingService
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.servlets.AttachmentDownloadServlet
|
||||
import core.node.servlets.DataUploadServlet
|
||||
import core.utilities.loggerFor
|
||||
@ -97,6 +98,7 @@ class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration
|
||||
resourceConfig.register(Config(services))
|
||||
resourceConfig.register(ResponseFilter())
|
||||
resourceConfig.register(api)
|
||||
resourceConfig.register(InterestRateSwapAPI(api))
|
||||
// Give the app a slightly better name in JMX rather than a randomly generated one and enable JMX
|
||||
resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
|
||||
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))
|
||||
|
@ -43,8 +43,8 @@ class MockNetworkMapService : NetworkMapService {
|
||||
override val partyNodes = Collections.synchronizedList(ArrayList<LegallyIdentifiableNode>())
|
||||
|
||||
init {
|
||||
partyNodes.add(LegallyIdentifiableNode(MockAddress("excalibur:8080"), Party("Excalibur", DummyPublicKey("Excalibur"))))
|
||||
partyNodes.add(LegallyIdentifiableNode(MockAddress("another:8080"), Party("ANOther", DummyPublicKey("ANOther"))))
|
||||
partyNodes.add(LegallyIdentifiableNode(MockAddress("bankC:8080"), Party("Bank C", DummyPublicKey("Bank C"))))
|
||||
partyNodes.add(LegallyIdentifiableNode(MockAddress("bankD:8080"), Party("Bank D", DummyPublicKey("Bank D"))))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,16 +43,21 @@ object NodeInterestRates {
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
|
||||
fun parseFixOf(key: String): FixOf {
|
||||
val (name, date, tenorString) = key.split(' ')
|
||||
val words = key.split(' ')
|
||||
val tenorString = words.last()
|
||||
val date = words.dropLast(1).last()
|
||||
val name = words.dropLast(2).joinToString(" ")
|
||||
return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
|
||||
}
|
||||
|
||||
/** Parses lines containing fixes */
|
||||
fun parseFile(s: String): Map<FixOf, Fix> {
|
||||
val results = HashMap<FixOf, Fix>()
|
||||
fun parseFile(s: String): Map<FixOf, TreeMap<LocalDate,Fix>> {
|
||||
val results = HashMap<FixOf, TreeMap<LocalDate,Fix>>()
|
||||
for (line in s.lines()) {
|
||||
val (fixOf, fix) = parseOneRate(line.trim())
|
||||
results[fixOf] = fix
|
||||
val (fixOf, fix) = parseOneRate(line)
|
||||
val genericKey = FixOf(fixOf.name, LocalDate.MIN, fixOf.ofTenor)
|
||||
val existingMap = results.computeIfAbsent(genericKey, { TreeMap() })
|
||||
existingMap[fixOf.forDay] = fix
|
||||
}
|
||||
return results
|
||||
}
|
||||
@ -91,14 +96,13 @@ object NodeInterestRates {
|
||||
override val acceptableFileExtensions = listOf(".rates", ".txt")
|
||||
|
||||
override fun upload(data: InputStream): String {
|
||||
val fixes: Map<FixOf, Fix> = data.
|
||||
val fixes: Map<FixOf, TreeMap<LocalDate,Fix>> = parseFile(data.
|
||||
bufferedReader().
|
||||
readLines().
|
||||
map { it.trim() }.
|
||||
// Filter out comment and empty lines.
|
||||
filterNot { it.startsWith("#") || it.isBlank() }.
|
||||
map { parseOneRate(it) }.
|
||||
associate { it.first to it.second }
|
||||
joinToString("\n"))
|
||||
|
||||
// TODO: Save the uploaded fixes to the storage service and reload on construction.
|
||||
|
||||
@ -106,12 +110,16 @@ object NodeInterestRates {
|
||||
// the pointer to the stack before working with the map.
|
||||
oracle.knownFixes = fixes
|
||||
|
||||
return "Accepted ${fixes.size} new interest rate fixes"
|
||||
val sumOfFixes = fixes.map { it.value.size }.sum()
|
||||
return "Accepted $sumOfFixes new interest rate fixes"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
||||
*
|
||||
* NOTE the implementation has changed such that it will find the nearest dated entry on OR BEFORE the date
|
||||
* requested so as not to need to populate every possible date in the flat file that (typically) feeds this service
|
||||
*/
|
||||
@ThreadSafe
|
||||
class Oracle(val identity: Party, private val signingKey: KeyPair) {
|
||||
@ -119,8 +127,13 @@ object NodeInterestRates {
|
||||
require(signingKey.public == identity.owningKey)
|
||||
}
|
||||
|
||||
/** The fix data being served by this oracle. */
|
||||
@Transient var knownFixes = emptyMap<FixOf, Fix>()
|
||||
/** The fix data being served by this oracle.
|
||||
*
|
||||
* This is now a map of FixOf (but with date set to LocalDate.MIN, so just index name and tenor)
|
||||
* to a sorted map of LocalDate to Fix, allowing for approximate date finding so that we do not need
|
||||
* to populate the file with a rate for every day.
|
||||
*/
|
||||
@Volatile var knownFixes = emptyMap<FixOf, TreeMap<LocalDate,Fix>>()
|
||||
set(value) {
|
||||
require(value.isNotEmpty())
|
||||
field = value
|
||||
@ -130,13 +143,24 @@ object NodeInterestRates {
|
||||
require(queries.isNotEmpty())
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
|
||||
val answers: List<Fix?> = queries.map { knownFixes[it] }
|
||||
val answers: List<Fix?> = queries.map { getKnownFix(knownFixes, it) }
|
||||
val firstNull = answers.indexOf(null)
|
||||
if (firstNull != -1)
|
||||
throw UnknownFix(queries[firstNull])
|
||||
return answers.filterNotNull()
|
||||
}
|
||||
|
||||
private fun getKnownFix(knownFixes: Map<FixOf, TreeMap<LocalDate, Fix>>, fixOf: FixOf): Fix? {
|
||||
val rates = knownFixes[FixOf(fixOf.name, LocalDate.MIN, fixOf.ofTenor)]
|
||||
// Greatest key less than or equal to the date we're looking for
|
||||
val floor = rates?.floorEntry(fixOf.forDay)?.value
|
||||
return if (floor!=null) {
|
||||
Fix(fixOf, floor.value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun sign(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
// Extract the fix commands marked as being signable by us.
|
||||
val fixes: List<Fix> = wtx.commands.
|
||||
@ -150,7 +174,7 @@ object NodeInterestRates {
|
||||
// For each fix, verify that the data is correct.
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
for (fix in fixes) {
|
||||
val known = knownFixes[fix.of]
|
||||
val known = getKnownFix(knownFixes, fix.of)
|
||||
if (known == null || known != fix)
|
||||
throw UnknownFix(fix.of)
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import contracts.Cash
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.NetworkMapService
|
||||
import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
@ -73,8 +72,8 @@ interface WalletService {
|
||||
*/
|
||||
val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||
|
||||
fun <T : LinearState> linearHeadsInstanceOf(clazz: Class<T>, predicate: (T) -> Boolean = { true } ): Map<SecureHash, StateAndRef<LinearState>> {
|
||||
return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) }
|
||||
fun <T : LinearState> linearHeadsInstanceOf(clazz: Class<T>, predicate: (T) -> Boolean = { true } ): Map<SecureHash, StateAndRef<T>> {
|
||||
return linearHeads.filterValues { clazz.isInstance(it.state) }.filterValues { predicate(it.state as T) }.mapValues { StateAndRef(it.value.state as T, it.value.ref) }
|
||||
}
|
||||
|
||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, ContractState?> {
|
||||
|
@ -48,7 +48,10 @@ abstract class ProtocolLogic<T> {
|
||||
return psm.sendAndReceive(topic, destination, sessionIDForSend, sessionIDForReceive, obj, T::class.java)
|
||||
}
|
||||
inline fun <reified T : Any> receive(topic: String, sessionIDForReceive: Long): UntrustworthyData<T> {
|
||||
return psm.receive(topic, sessionIDForReceive, T::class.java)
|
||||
return receive(topic, sessionIDForReceive, T::class.java)
|
||||
}
|
||||
@Suspendable fun <T : Any> receive(topic: String, sessionIDForReceive: Long, clazz: Class<T>): UntrustworthyData<T> {
|
||||
return psm.receive(topic, sessionIDForReceive, clazz)
|
||||
}
|
||||
@Suspendable fun send(topic: String, destination: MessageRecipients, sessionID: Long, obj: Any) {
|
||||
psm.send(topic, destination, sessionID, obj)
|
||||
|
182
src/main/kotlin/demos/IRSDemo.kt
Normal file
182
src/main/kotlin/demos/IRSDemo.kt
Normal file
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package demos
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import core.Party
|
||||
import core.logElapsedTime
|
||||
import core.node.Node
|
||||
import core.node.NodeConfiguration
|
||||
import core.node.NodeConfigurationFromConfig
|
||||
import core.node.services.ArtemisMessagingService
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.MockNetworkMapService
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.BriefLogFormatter
|
||||
import joptsimple.OptionParser
|
||||
import demos.protocols.AutoOfferProtocol
|
||||
import demos.protocols.ExitServerProtocol
|
||||
import demos.protocols.UpdateBusinessDayProtocol
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
// IRS DEMO
|
||||
//
|
||||
// TODO: Please see TBD
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val parser = OptionParser()
|
||||
val networkAddressArg = parser.accepts("network-address").withRequiredArg().required()
|
||||
val dirArg = parser.accepts("directory").withRequiredArg().defaultsTo("nodedata")
|
||||
|
||||
// Temporary flags until network map and service discovery is fleshed out. The identity file does NOT contain the
|
||||
// network address because all this stuff is meant to come from a dynamic discovery service anyway, and the identity
|
||||
// is meant to be long-term stable. It could contain a domain name, but we may end up not routing messages directly
|
||||
// to DNS-identified endpoints anyway (e.g. consider onion routing as a possibility).
|
||||
val timestamperIdentityFile = parser.accepts("timestamper-identity-file").withRequiredArg().required()
|
||||
val timestamperNetAddr = parser.accepts("timestamper-address").requiredIf(timestamperIdentityFile).withRequiredArg()
|
||||
|
||||
val rateOracleIdentityFile = parser.accepts("rates-oracle-identity-file").withRequiredArg().required()
|
||||
val rateOracleNetAddr = parser.accepts("rates-oracle-address").requiredIf(rateOracleIdentityFile).withRequiredArg()
|
||||
|
||||
// Use these to list one or more peers (again, will be superseded by discovery implementation)
|
||||
val fakeTradeWithAddr = parser.accepts("fake-trade-with-address").withRequiredArg().required()
|
||||
val fakeTradeWithIdentityFile = parser.accepts("fake-trade-with-identity-file").withRequiredArg().required()
|
||||
|
||||
val options = try {
|
||||
parser.parse(*args)
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
printHelp()
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// Suppress the Artemis MQ noise, and activate the demo logging.
|
||||
BriefLogFormatter.initVerbose("+demo.irsdemo", "-org.apache.activemq")
|
||||
|
||||
val dir = Paths.get(options.valueOf(dirArg))
|
||||
val configFile = dir.resolve("config")
|
||||
|
||||
if (!Files.exists(dir)) {
|
||||
Files.createDirectory(dir)
|
||||
}
|
||||
|
||||
val config = loadConfigFile(configFile)
|
||||
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT)
|
||||
|
||||
// The timestamping node runs in the same process as the one that passes null to Node constructor.
|
||||
val timestamperId = if(options.valueOf(timestamperNetAddr).equals(options.valueOf(networkAddressArg))) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
legallyIdentifiableNode(options.valueOf(timestamperNetAddr), options.valueOf(timestamperIdentityFile))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// The timestamping node runs in the same process as the one that passes null to Node constructor.
|
||||
val rateOracleId = if(options.valueOf(rateOracleNetAddr).equals(options.valueOf(networkAddressArg))) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
legallyIdentifiableNode(options.valueOf(rateOracleNetAddr), options.valueOf(rateOracleIdentityFile))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val node = logElapsedTime("Node startup") { Node(dir, myNetAddr, config, timestamperId, DemoClock()).start() }
|
||||
|
||||
// Add self to network map
|
||||
(node.services.networkMapService as MockNetworkMapService).partyNodes.add(node.legallyIdentifiableAddress)
|
||||
|
||||
// Add rates oracle to network map
|
||||
(node.services.networkMapService as MockNetworkMapService).ratesOracleNodes.add(rateOracleId)
|
||||
|
||||
val hostAndPortStrings = options.valuesOf(fakeTradeWithAddr)
|
||||
val identityFiles = options.valuesOf(fakeTradeWithIdentityFile)
|
||||
if(hostAndPortStrings.size != identityFiles.size) {
|
||||
throw IllegalArgumentException("Different number of peer addresses (${hostAndPortStrings.size}) and identities (${identityFiles.size})")
|
||||
}
|
||||
for ((hostAndPortString,identityFile) in hostAndPortStrings.zip(identityFiles)) {
|
||||
try {
|
||||
val peerId = legallyIdentifiableNode(hostAndPortString, identityFile)
|
||||
(node.services.networkMapService as MockNetworkMapService).partyNodes.add(peerId)
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
// Register handlers for the demo
|
||||
AutoOfferProtocol.Handler.register(node)
|
||||
UpdateBusinessDayProtocol.Handler.register(node)
|
||||
ExitServerProtocol.Handler.register(node)
|
||||
|
||||
while(true) {
|
||||
Thread.sleep(1000L)
|
||||
}
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
fun legallyIdentifiableNode(hostAndPortString: String, identityFile: String): LegallyIdentifiableNode {
|
||||
try {
|
||||
val addr = HostAndPort.fromString(hostAndPortString).withDefaultPort(Node.DEFAULT_PORT)
|
||||
val path = Paths.get(identityFile)
|
||||
val party = Files.readAllBytes(path).deserialize<Party>(includeClassName = true)
|
||||
return LegallyIdentifiableNode(ArtemisMessagingService.makeRecipient(addr), party)
|
||||
} catch (e: Exception) {
|
||||
println("Could not find identify file $identityFile. If the file has just been created as part of starting the demo, please restart this node")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadConfigFile(configFile: Path): NodeConfiguration {
|
||||
fun askAdminToEditConfig(configFile: Path?) {
|
||||
println()
|
||||
println("This is the first run, so you should edit the config file in $configFile and then start the node again.")
|
||||
println()
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val defaultLegalName = "Global MegaCorp, Ltd."
|
||||
|
||||
if (!Files.exists(configFile)) {
|
||||
createDefaultConfigFile(configFile, defaultLegalName)
|
||||
askAdminToEditConfig(configFile)
|
||||
}
|
||||
|
||||
System.setProperty("config.file", configFile.toAbsolutePath().toString())
|
||||
val config = NodeConfigurationFromConfig(ConfigFactory.load())
|
||||
|
||||
// Make sure admin did actually edit at least the legal name.
|
||||
if (config.myLegalName == defaultLegalName)
|
||||
askAdminToEditConfig(configFile)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
private fun createDefaultConfigFile(configFile: Path?, defaultLegalName: String) {
|
||||
Files.write(configFile,
|
||||
"""
|
||||
# Node configuration: give the buyer node the name 'Bank of Zurich' (no quotes)
|
||||
# The seller node can be named whatever you like.
|
||||
|
||||
myLegalName = $defaultLegalName
|
||||
""".trimIndent().toByteArray())
|
||||
}
|
||||
|
||||
private fun printHelp() {
|
||||
println("""
|
||||
Please refer to the documentation that doesn't yet exist to learn how to run the demo.
|
||||
""".trimIndent())
|
||||
}
|
@ -85,8 +85,8 @@ fun main(args: Array<String>) {
|
||||
val myNetAddr = HostAndPort.fromString(options.valueOf(networkAddressArg)).withDefaultPort(Node.DEFAULT_PORT)
|
||||
val listening = options.has(serviceFakeTradesArg)
|
||||
|
||||
if (listening && config.myLegalName != "Bank of Zurich") {
|
||||
println("The buyer node must have a legal name of 'Bank of Zurich'. Please edit the config file.")
|
||||
if (listening && config.myLegalName != "Bank A") {
|
||||
println("The buyer node must have a legal name of 'Bank A'. Please edit the config file.")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
|
120
src/main/kotlin/demos/protocols/AutoOfferProtocol.kt
Normal file
120
src/main/kotlin/demos/protocols/AutoOfferProtocol.kt
Normal file
@ -0,0 +1,120 @@
|
||||
package demos.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import contracts.DealState
|
||||
import core.Party
|
||||
import core.SignedTransaction
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.Node
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.random63BitValue
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
import core.utilities.ProgressTracker
|
||||
import protocols.TwoPartyDealProtocol
|
||||
|
||||
/**
|
||||
* This whole class is really part of a demo just to initiate the agreement of a deal with a simple
|
||||
* API call from a single party without bi-directional access to the database of offers etc.
|
||||
*
|
||||
* In the "real world", we'd probably have the offers sitting in the platform prior to the agreement step
|
||||
* or the protocol would have to reach out to external systems (or users) to verify the deals
|
||||
*/
|
||||
object AutoOfferProtocol {
|
||||
val TOPIC = "autooffer.topic"
|
||||
|
||||
data class AutoOfferMessage(val otherSide: SingleMessageRecipient,
|
||||
val otherSessionID: Long, val dealBeingOffered: DealState)
|
||||
|
||||
object Handler {
|
||||
|
||||
object RECEIVED : ProgressTracker.Step("Received offer")
|
||||
object DEALING : ProgressTracker.Step("Starting the deal protocol")
|
||||
|
||||
fun tracker() = ProgressTracker(RECEIVED, DEALING).apply {
|
||||
childrenFor[DEALING] = TwoPartyDealProtocol.Primary.tracker()
|
||||
}
|
||||
|
||||
class Callback(val success: (SignedTransaction) -> Unit) : FutureCallback<SignedTransaction> {
|
||||
override fun onFailure(t: Throwable?) {
|
||||
// TODO handle exceptions
|
||||
}
|
||||
|
||||
override fun onSuccess(st: SignedTransaction?) {
|
||||
success(st!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
val progressTracker = tracker()
|
||||
ANSIProgressRenderer.progressTracker = progressTracker
|
||||
progressTracker.currentStep = RECEIVED
|
||||
val autoOfferMessage = msg.data.deserialize<AutoOfferMessage>()
|
||||
// Put the deal onto the ledger
|
||||
progressTracker.currentStep = DEALING
|
||||
// TODO required as messaging layer does not currently queue messages that arrive before we expect them
|
||||
Thread.sleep(100)
|
||||
val seller = TwoPartyDealProtocol.Instigator(autoOfferMessage.otherSide, node.timestamperAddress!!,
|
||||
autoOfferMessage.dealBeingOffered, node.services.keyManagementService.freshKey(), autoOfferMessage.otherSessionID, progressTracker.childrenFor[DEALING]!!)
|
||||
val future = node.smm.add("${TwoPartyDealProtocol.DEAL_TOPIC}.seller", seller)
|
||||
// This is required because we are doing child progress outside of a subprotocol. In future, we should just wrap things like this in a protocol to avoid it
|
||||
Futures.addCallback(future, Callback() {
|
||||
seller.progressTracker.currentStep = ProgressTracker.DONE
|
||||
progressTracker.currentStep = ProgressTracker.DONE
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Requester<T>(val dealToBeOffered: DealState) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVED : ProgressTracker.Step("Received API call")
|
||||
object ANNOUNCING : ProgressTracker.Step("Announcing to the peer node")
|
||||
object DEALING : ProgressTracker.Step("Starting the deal protocol")
|
||||
|
||||
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol involved at some
|
||||
// point: by setting up the tracker in advance, the user can see what's coming in more detail, instead of being
|
||||
// surprised when it appears as a new set of tasks below the current one.
|
||||
fun tracker() = ProgressTracker(RECEIVED, ANNOUNCING, DEALING).apply {
|
||||
childrenFor[DEALING] = TwoPartyDealProtocol.Secondary.tracker()
|
||||
}
|
||||
}
|
||||
|
||||
override val progressTracker = tracker()
|
||||
|
||||
init {
|
||||
progressTracker.currentStep = RECEIVED
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val ourSessionID = random63BitValue()
|
||||
|
||||
val timestampingAuthority = serviceHub.networkMapService.timestampingNodes[0]
|
||||
// need to pick which ever party is not us
|
||||
val otherParty = notUs(*dealToBeOffered.parties).single()
|
||||
val otherSide = (serviceHub.networkMapService.nodeForPartyName(otherParty.name))!!.address
|
||||
progressTracker.currentStep = ANNOUNCING
|
||||
send(TOPIC, otherSide, 0, AutoOfferMessage(serviceHub.networkService.myAddress, ourSessionID, dealToBeOffered))
|
||||
progressTracker.currentStep = DEALING
|
||||
val stx = subProtocol(TwoPartyDealProtocol.Acceptor(otherSide, timestampingAuthority.identity, dealToBeOffered, ourSessionID, progressTracker.childrenFor[DEALING]!!))
|
||||
return stx
|
||||
}
|
||||
|
||||
fun notUs(vararg parties: Party): List<Party> {
|
||||
val notUsParties : MutableList<Party> = arrayListOf()
|
||||
for(party in parties) {
|
||||
if (serviceHub.storageService.myLegalIdentity != party) {
|
||||
notUsParties.add(party)
|
||||
}
|
||||
}
|
||||
return notUsParties
|
||||
}
|
||||
|
||||
}
|
||||
}
|
71
src/main/kotlin/demos/protocols/ExitServerProtocol.kt
Normal file
71
src/main/kotlin/demos/protocols/ExitServerProtocol.kt
Normal file
@ -0,0 +1,71 @@
|
||||
package demos.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import core.node.Node
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.MockNetworkMapService
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.deserialize
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
object ExitServerProtocol {
|
||||
|
||||
val TOPIC = "exit.topic"
|
||||
|
||||
// Will only be enabled if you install the Handler
|
||||
@Volatile private var enabled = false
|
||||
|
||||
data class ExitMessage(val exitCode: Int)
|
||||
|
||||
object Handler {
|
||||
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
// Just to validate we got the message
|
||||
if(enabled) {
|
||||
val message = msg.data.deserialize<ExitMessage>()
|
||||
System.exit(message.exitCode)
|
||||
}
|
||||
}
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This takes a Java Integer rather than Kotlin Int as that is what we end up with in the calling map and currently
|
||||
* we do not support coercing numeric types in the reflective search for matching constructors
|
||||
*/
|
||||
class Broadcast(val exitCode: Integer) : ProtocolLogic<Boolean>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
if(enabled) {
|
||||
val rc = exitCode.toInt()
|
||||
val message = ExitMessage(rc)
|
||||
|
||||
for (recipient in serviceHub.networkMapService.partyNodes) {
|
||||
doNextRecipient(recipient, message)
|
||||
}
|
||||
// Sleep a little in case any async message delivery to other nodes needs to happen
|
||||
Strand.sleep(1, TimeUnit.SECONDS)
|
||||
System.exit(rc)
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun doNextRecipient(recipient: LegallyIdentifiableNode, message: ExitMessage) {
|
||||
if(recipient.address is MockNetworkMapService.MockAddress) {
|
||||
// Ignore
|
||||
} else {
|
||||
// TODO: messaging ourselves seems to trigger a bug for the time being and we continuously receive messages
|
||||
if (recipient.identity != serviceHub.storageService.myLegalIdentity) {
|
||||
send(TOPIC, recipient.address, 0, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
174
src/main/kotlin/demos/protocols/UpdateBusinessDayProtocol.kt
Normal file
174
src/main/kotlin/demos/protocols/UpdateBusinessDayProtocol.kt
Normal file
@ -0,0 +1,174 @@
|
||||
package demos.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import contracts.DealState
|
||||
import contracts.InterestRateSwap
|
||||
import core.StateAndRef
|
||||
import core.node.Node
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.MockNetworkMapService
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.random63BitValue
|
||||
import core.serialization.deserialize
|
||||
import core.utilities.ANSIProgressRenderer
|
||||
import core.utilities.ProgressTracker
|
||||
import demos.DemoClock
|
||||
import protocols.TwoPartyDealProtocol
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* This is a very temporary, demo-oriented way of initiating processing of temporal events and is not
|
||||
* intended as the way things will necessarily be done longer term
|
||||
*/
|
||||
object UpdateBusinessDayProtocol {
|
||||
|
||||
val TOPIC = "businessday.topic"
|
||||
|
||||
class Updater(val date: LocalDate, val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Updater.tracker()) : ProtocolLogic<Boolean>() {
|
||||
|
||||
companion object {
|
||||
object FETCHING : ProgressTracker.Step("Fetching deals")
|
||||
object ITERATING_DEALS : ProgressTracker.Step("Interating over deals")
|
||||
object ITERATING_FIXINGS : ProgressTracker.Step("Iterating over fixings")
|
||||
object FIXING : ProgressTracker.Step("Fixing deal")
|
||||
|
||||
fun tracker() = ProgressTracker(FETCHING, ITERATING_DEALS, ITERATING_FIXINGS, FIXING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
// Get deals
|
||||
progressTracker.currentStep = FETCHING
|
||||
val dealStateRefs = serviceHub.walletService.linearHeadsInstanceOf(DealState::class.java)
|
||||
val otherPartyToDeals = dealStateRefs.values.groupBy { otherParty(it.state) }
|
||||
|
||||
// TODO we need to process these in parallel to stop there being an ordering problem across more than two nodes
|
||||
val sortedParties = otherPartyToDeals.keys.sortedBy { it.identity.name }
|
||||
for (party in sortedParties) {
|
||||
val sortedDeals = otherPartyToDeals[party]!!.sortedBy { it.state.ref }
|
||||
for(deal in sortedDeals) {
|
||||
progressTracker.currentStep = ITERATING_DEALS
|
||||
processDeal(party, deal, date, sessionID)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This assumes we definitely have one key or the other
|
||||
fun otherParty(deal: DealState): LegallyIdentifiableNode {
|
||||
val ourKeys = serviceHub.keyManagementService.keys.keys
|
||||
return serviceHub.networkMapService.nodeForPartyName(deal.parties.single { !ourKeys.contains(it.owningKey) }.name)!!
|
||||
}
|
||||
|
||||
// TODO we should make this more object oriented when we can ask a state for it's contract
|
||||
@Suspendable
|
||||
fun processDeal(party: LegallyIdentifiableNode, deal: StateAndRef<DealState>, date: LocalDate, sessionID: Long) {
|
||||
when(deal.state) {
|
||||
is InterestRateSwap.State -> processInterestRateSwap(party, StateAndRef(deal.state as InterestRateSwap.State, deal.ref), date, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO and this would move to the InterestRateSwap and cope with permutations of Fixed/Floating and Floating/Floating etc
|
||||
@Suspendable
|
||||
fun processInterestRateSwap(party: LegallyIdentifiableNode, deal: StateAndRef<InterestRateSwap.State>, date: LocalDate, sessionID: Long) {
|
||||
var dealStateAndRef: StateAndRef<InterestRateSwap.State>? = deal
|
||||
var nextFixingDate = deal.state.calculation.nextFixingDate()
|
||||
while (nextFixingDate != null && !nextFixingDate.isAfter(date)) {
|
||||
progressTracker.currentStep = ITERATING_FIXINGS
|
||||
/*
|
||||
* Note that this choice of fixed versus floating leg is simply to assign roles in
|
||||
* the fixing protocol and doesn't infer roles or responsibilities in a business sense.
|
||||
* One of the parties needs to take the lead in the coordination and this is a reliable deterministic way
|
||||
* to do it.
|
||||
*/
|
||||
if (party.identity.name == deal.state.fixedLeg.fixedRatePayer.name) {
|
||||
dealStateAndRef = nextFixingFloatingLeg(dealStateAndRef!!, party, sessionID)
|
||||
} else {
|
||||
dealStateAndRef = nextFixingFixedLeg(dealStateAndRef!!, party, sessionID)
|
||||
}
|
||||
nextFixingDate = dealStateAndRef?.state?.calculation?.nextFixingDate()
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun nextFixingFloatingLeg(dealStateAndRef: StateAndRef<InterestRateSwap.State>, party: LegallyIdentifiableNode, sessionID: Long): StateAndRef<InterestRateSwap.State>? {
|
||||
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Primary.tracker()
|
||||
progressTracker.currentStep = FIXING
|
||||
|
||||
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapService.timestampingNodes[0], dealStateAndRef, serviceHub.keyManagementService.freshKey(), sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||
Strand.sleep(100)
|
||||
val result = subProtocol(participant)
|
||||
return result.tx.outRef(0)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun nextFixingFixedLeg(dealStateAndRef: StateAndRef<InterestRateSwap.State>, party: LegallyIdentifiableNode, sessionID: Long): StateAndRef<InterestRateSwap.State>? {
|
||||
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Secondary.tracker()
|
||||
progressTracker.currentStep = FIXING
|
||||
|
||||
val participant = TwoPartyDealProtocol.Fixer(party.address, serviceHub.networkMapService.timestampingNodes[0].identity, dealStateAndRef, sessionID, progressTracker.childrenFor[FIXING]!!)
|
||||
val result = subProtocol(participant)
|
||||
return result.tx.outRef(0)
|
||||
}
|
||||
}
|
||||
|
||||
data class UpdateBusinessDayMessage(val date: LocalDate, val sessionID: Long)
|
||||
|
||||
object Handler {
|
||||
|
||||
fun register(node: Node) {
|
||||
node.net.addMessageHandler("${TOPIC}.0") { msg, registration ->
|
||||
// Just to validate we got the message
|
||||
val updateBusinessDayMessage = msg.data.deserialize<UpdateBusinessDayMessage>()
|
||||
if ((node.services.clock as DemoClock).updateDate(updateBusinessDayMessage.date)) {
|
||||
val participant = Updater(updateBusinessDayMessage.date, updateBusinessDayMessage.sessionID)
|
||||
ANSIProgressRenderer.progressTracker = participant.progressTracker
|
||||
node.smm.add("update.business.day", participant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast(val date: LocalDate,
|
||||
override val progressTracker: ProgressTracker = Broadcast.tracker()) : ProtocolLogic<Boolean>() {
|
||||
|
||||
companion object {
|
||||
object NOTIFYING : ProgressTracker.Step("Notifying peer")
|
||||
object LOCAL : ProgressTracker.Step("Updating locally")
|
||||
|
||||
fun tracker() = ProgressTracker(NOTIFYING, LOCAL).apply {
|
||||
childrenFor[LOCAL] = Updater.tracker()
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
val message = UpdateBusinessDayMessage(date, random63BitValue())
|
||||
|
||||
for (recipient in serviceHub.networkMapService.partyNodes) {
|
||||
progressTracker.currentStep = NOTIFYING
|
||||
doNextRecipient(recipient, message)
|
||||
}
|
||||
if ((serviceHub.clock as DemoClock).updateDate(message.date)) {
|
||||
progressTracker.currentStep = LOCAL
|
||||
subProtocol(Updater(message.date, message.sessionID, progressTracker.childrenFor[LOCAL]!!))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun doNextRecipient(recipient: LegallyIdentifiableNode, message: UpdateBusinessDayMessage) {
|
||||
if(recipient.address is MockNetworkMapService.MockAddress) {
|
||||
// Ignore
|
||||
} else {
|
||||
// TODO: messaging ourselves seems to trigger a bug for the time being and we continuously receive messages
|
||||
if (recipient.identity != serviceHub.storageService.myLegalIdentity) {
|
||||
send(TOPIC, recipient.address, 0, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -11,8 +11,8 @@ package protocols
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.utilities.ProgressTracker
|
||||
import java.math.BigDecimal
|
||||
@ -50,7 +50,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = progressTracker.steps[0]
|
||||
progressTracker.currentStep = progressTracker.steps[1]
|
||||
val fix = query()
|
||||
progressTracker.currentStep = WORKING
|
||||
checkFixIsNearExpected(fix)
|
||||
|
@ -12,9 +12,9 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import core.Party
|
||||
import core.WireTransaction
|
||||
import core.crypto.DigitalSignature
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.MessageRecipients
|
||||
import core.messaging.StateMachineManager
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.node.services.NodeTimestamperService
|
||||
import core.node.services.TimestamperService
|
||||
import core.protocols.ProtocolLogic
|
||||
|
395
src/main/kotlin/protocols/TwoPartyDealProtocol.kt
Normal file
395
src/main/kotlin/protocols/TwoPartyDealProtocol.kt
Normal file
@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import contracts.DealState
|
||||
import contracts.FixableDealState
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.signWithECDSA
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.UntrustworthyData
|
||||
import core.utilities.trace
|
||||
import java.math.BigDecimal
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* Classes for manipulating a two party deal or agreement.
|
||||
*
|
||||
* TODO: The subclasses should probably be broken out into individual protocols rather than making this an ever expanding collection of subclasses.
|
||||
*
|
||||
* TODO: Also, the term Deal is used here where we might prefer Agreement.
|
||||
*
|
||||
*/
|
||||
object TwoPartyDealProtocol {
|
||||
val DEAL_TOPIC = "platform.deal"
|
||||
|
||||
class DealMismatchException(val expectedDeal: ContractState, val actualDeal: ContractState) : Exception() {
|
||||
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
||||
}
|
||||
|
||||
class DealRefMismatchException(val expectedDeal: StateRef, val actualDeal: StateRef) : Exception() {
|
||||
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
||||
}
|
||||
|
||||
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
|
||||
data class Handshake<T>(
|
||||
val payload: T,
|
||||
val publicKey: PublicKey,
|
||||
val sessionID: Long
|
||||
)
|
||||
|
||||
class SignaturesFromPrimary(val timestampAuthoritySig: DigitalSignature.WithKey, val sellerSig: DigitalSignature.WithKey)
|
||||
|
||||
/**
|
||||
* Abstracted bilateral deal protocol participant that initiates communication/handshake.
|
||||
*
|
||||
* There's a good chance we can push at least some of this logic down into core protocol logic
|
||||
* and helper methods etc.
|
||||
*/
|
||||
abstract class Primary<U>(val payload: U,
|
||||
val otherSide: SingleMessageRecipient,
|
||||
val otherSessionID: Long,
|
||||
val myKeyPair: KeyPair,
|
||||
val timestampingAuthority: LegallyIdentifiableNode,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal from other")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal from other")
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
object TIMESTAMPING : ProgressTracker.Step("Timestamping transaction")
|
||||
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to other party")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, TIMESTAMPING, SENDING_SIGS, RECORDING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun getPartialTransaction(): UntrustworthyData<SignedTransaction> {
|
||||
progressTracker.currentStep = AWAITING_PROPOSAL
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = Handshake<U>(payload, myKeyPair.public, sessionID)
|
||||
|
||||
val maybeSTX = sendAndReceive<SignedTransaction>(DEAL_TOPIC, otherSide, otherSessionID, sessionID, hello)
|
||||
|
||||
return maybeSTX
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
untrustedPartialTX.validate {
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||
if (missingSigs != setOf(myKeyPair.public, timestampingAuthority.identity.owningKey))
|
||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
||||
|
||||
val wtx: WireTransaction = it.tx
|
||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||
|
||||
checkDependencies(it)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
|
||||
|
||||
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
|
||||
//
|
||||
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
|
||||
// we're reusing keys! So don't reuse keys!
|
||||
// - This tx may include output states that impose odd conditions on the movement of the cash,
|
||||
// once we implement state pairing.
|
||||
//
|
||||
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependencies(stx: SignedTransaction) {
|
||||
// Download and check all the transactions that this transaction depends on, but do not check this
|
||||
// transaction itself.
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val partialTX: SignedTransaction = verifyPartialTransaction(getPartialTransaction())
|
||||
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = signWithOurKey(partialTX)
|
||||
val tsaSig = timestamp(partialTX)
|
||||
|
||||
val fullySigned = sendSignatures(partialTX, ourSignature, tsaSig)
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
|
||||
serviceHub.recordTransactions(listOf(fullySigned))
|
||||
|
||||
logger.trace { "Deal stored" }
|
||||
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun timestamp(partialTX: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.childrenFor[TIMESTAMPING] = TimestampingProtocol.tracker()
|
||||
progressTracker.currentStep = TIMESTAMPING
|
||||
return subProtocol(TimestampingProtocol(timestampingAuthority, partialTX.txBits, progressTracker.childrenFor[TIMESTAMPING]!!))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
progressTracker.currentStep = SIGNING
|
||||
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun sendSignatures(partialTX: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
tsaSig: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_SIGS
|
||||
val fullySigned = partialTX + tsaSig + ourSignature
|
||||
|
||||
logger.trace { "Built finished transaction, sending back to other party!" }
|
||||
|
||||
send(DEAL_TOPIC, otherSide, otherSessionID, SignaturesFromPrimary(tsaSig, ourSignature))
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abstracted bilateral deal protocol participant that is recipient of initial communication.
|
||||
*
|
||||
* There's a good chance we can push at least some of this logic down into core protocol logic
|
||||
* and helper methods etc.
|
||||
*/
|
||||
abstract class Secondary<U>(val otherSide: SingleMessageRecipient,
|
||||
val timestampingAuthority: Party,
|
||||
val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for deal info")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying deal info")
|
||||
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
|
||||
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the other party")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction")
|
||||
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES, RECORDING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val handshake = receiveAndValidateHandshake()
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
val (ptx, additionalSigningPubKeys) = assembleSharedTX(handshake)
|
||||
val stx = signWithOurKeys(additionalSigningPubKeys, ptx)
|
||||
|
||||
val signatures = swapSignaturesWithPrimary(stx, handshake.sessionID)
|
||||
|
||||
logger.trace { "Got signatures from other party, verifying ... " }
|
||||
val fullySigned = stx + signatures.timestampAuthoritySig + signatures.sellerSig
|
||||
fullySigned.verify()
|
||||
|
||||
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
serviceHub.recordTransactions(listOf(fullySigned))
|
||||
|
||||
logger.trace { "Deal transaction stored" }
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndValidateHandshake(): Handshake<U> {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Wait for a trade request to come in on our pre-provided session ID.
|
||||
val handshake = receive(DEAL_TOPIC, sessionID, Handshake::class.java)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
handshake.validate {
|
||||
return validateHandshake(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun swapSignaturesWithPrimary(stx: SignedTransaction, theirSessionID: Long): SignaturesFromPrimary {
|
||||
progressTracker.currentStep = SWAPPING_SIGNATURES
|
||||
logger.trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||
|
||||
return sendAndReceive<SignaturesFromPrimary>(DEAL_TOPIC, otherSide, theirSessionID, sessionID, stx).validate { it }
|
||||
}
|
||||
|
||||
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (k in signingPubKeys) {
|
||||
val priv = serviceHub.keyManagementService.toPrivate(k)
|
||||
ptx.signWith(KeyPair(k, priv))
|
||||
}
|
||||
|
||||
return ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
}
|
||||
|
||||
@Suspendable protected abstract fun validateHandshake(handshake: Handshake<*>): Handshake<U>
|
||||
@Suspendable protected abstract fun assembleSharedTX(handshake: Handshake<U>): Pair<TransactionBuilder, List<PublicKey>>
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the protocol for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Instigator<T : DealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: LegallyIdentifiableNode,
|
||||
dealBeingOffered: T,
|
||||
myKeyPair: KeyPair,
|
||||
buyerSessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<T>(dealBeingOffered, otherSide, buyerSessionID, myKeyPair, timestampingAuthority)
|
||||
|
||||
/**
|
||||
* One side of the protocol for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Acceptor<T : DealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: Party,
|
||||
val dealToBuy: T,
|
||||
sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<T>(otherSide, timestampingAuthority, sessionID) {
|
||||
|
||||
@Suspendable
|
||||
override fun validateHandshake(handshake: Handshake<*>): Handshake<T> {
|
||||
with(handshake as Handshake<T>) {
|
||||
// What is the seller trying to sell us?
|
||||
val deal = handshake.payload
|
||||
val otherKey = handshake.publicKey
|
||||
logger.trace { "Got deal request for: ${handshake.payload}" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
check(handshake.sessionID > 0)
|
||||
if (dealToBuy != deal)
|
||||
throw DealMismatchException(dealToBuy, deal)
|
||||
|
||||
// We need to substitute in the new public keys for the Parties
|
||||
val myName = serviceHub.storageService.myLegalIdentity.name
|
||||
val myOldParty = deal.parties.single { it.name == myName }
|
||||
val theirOldParty = deal.parties.single { it.name != myName }
|
||||
|
||||
val newDeal = deal.withPublicKey(myOldParty, serviceHub.keyManagementService.freshKey().public).withPublicKey(theirOldParty, otherKey) as T
|
||||
|
||||
return handshake.copy(payload = newDeal)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleSharedTX(handshake: Handshake<T>): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val ptx = handshake.payload.generateAgreement()
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(serviceHub.clock.instant(), timestampingAuthority, 30.seconds)
|
||||
return Pair(ptx, arrayListOf(handshake.payload.parties.single { it.name == serviceHub.storageService.myLegalIdentity.name }.owningKey))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the fixing protocol for an interest rate swap, but could easily be generalised further.
|
||||
*
|
||||
* Do not infer too much from the name of the class. This is just to indicate that it is the "side"
|
||||
* of the protocol that is run by the party with the fixed leg of swap deal, which is the basis for decided
|
||||
* who does what in the protocol.
|
||||
*/
|
||||
open class Fixer<T : FixableDealState>(otherSide: SingleMessageRecipient,
|
||||
timestampingAuthority: Party,
|
||||
val dealToFix: StateAndRef<T>,
|
||||
sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Secondary.tracker()) : Secondary<StateRef>(otherSide, timestampingAuthority, sessionID) {
|
||||
|
||||
@Suspendable
|
||||
override fun validateHandshake(handshake: Handshake<*>): Handshake<StateRef> {
|
||||
with(handshake as Handshake<StateRef>) {
|
||||
logger.trace { "Got fixing request for: ${dealToFix.state}" }
|
||||
|
||||
// Check the start message for acceptability.
|
||||
if (dealToFix.ref != handshake.payload)
|
||||
throw DealRefMismatchException(dealToFix.ref, handshake.payload)
|
||||
|
||||
// Check the transaction that contains the state which is being resolved.
|
||||
// We only have a hash here, so if we don't know it already, we have to ask for it.
|
||||
//subProtocol(ResolveTransactionsProtocol(setOf(handshake.payload.txhash), otherSide))
|
||||
|
||||
return handshake
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun assembleSharedTX(handshake: Handshake<StateRef>): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val fixOf = dealToFix.state.nextFixingOf()!!
|
||||
|
||||
// TODO Do we need/want to substitute in new public keys for the Parties?
|
||||
val myName = serviceHub.storageService.myLegalIdentity.name
|
||||
val deal = dealToFix.state
|
||||
val myOldParty = deal.parties.single { it.name == myName }
|
||||
val theirOldParty = deal.parties.single { it.name != myName }
|
||||
val myNewKey = serviceHub.keyManagementService.freshKey().public
|
||||
|
||||
val newDeal = deal.withPublicKey(myOldParty, myNewKey).withPublicKey(theirOldParty, handshake.publicKey) as T
|
||||
val oldRef = dealToFix.ref
|
||||
|
||||
val ptx = TransactionBuilder()
|
||||
val addFixing = object : RatesFixProtocol(ptx, serviceHub.networkMapService.ratesOracleNodes[0], fixOf, BigDecimal.ZERO, BigDecimal.ONE) {
|
||||
|
||||
@Suspendable
|
||||
override fun beforeSigning(fix: Fix) {
|
||||
newDeal.generateFix(ptx, oldRef, fix)
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
ptx.setTime(serviceHub.clock.instant(), timestampingAuthority, 30.seconds)
|
||||
}
|
||||
|
||||
}
|
||||
subProtocol(addFixing)
|
||||
|
||||
return Pair(ptx, arrayListOf(myNewKey))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One side of the fixing protocol for an interest rate swap, but could easily be generalised furher
|
||||
*
|
||||
* As per the [Fixer], do not infer too much from this class name in terms of business roles. This
|
||||
* is just the "side" of the protocol run by the party with the floating leg as a way of deciding who
|
||||
* does what in the protocol.
|
||||
*/
|
||||
open class Floater<T : FixableDealState>(otherSide: SingleMessageRecipient,
|
||||
otherSessionID: Long,
|
||||
timestampingAuthority: LegallyIdentifiableNode,
|
||||
dealToFix: StateAndRef<T>,
|
||||
myKeyPair: KeyPair,
|
||||
val sessionID: Long,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary<StateRef>(dealToFix.ref, otherSide, otherSessionID, myKeyPair, timestampingAuthority)
|
||||
}
|
@ -15,10 +15,9 @@ import contracts.sumCashBy
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.signWithECDSA
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.messaging.SingleMessageRecipient
|
||||
import core.messaging.StateMachineManager
|
||||
import protocols.TimestampingProtocol
|
||||
import core.node.services.LegallyIdentifiableNode
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.utilities.ProgressTracker
|
||||
import core.utilities.trace
|
||||
@ -26,7 +25,6 @@ import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.time.Instant
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
|
||||
|
@ -11,8 +11,6 @@ package core
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import core.crypto.*
|
||||
import core.messaging.MessagingService
|
||||
import core.node.services.MockNetworkMapService
|
||||
import core.node.services.NetworkMapService
|
||||
import core.node.services.*
|
||||
import core.serialization.SerializedBytes
|
||||
import core.serialization.deserialize
|
||||
|
@ -11,7 +11,8 @@ package core.node
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import core.*
|
||||
import core.crypto.SecureHash
|
||||
import core.messaging.*
|
||||
import core.messaging.StateMachineManager
|
||||
import core.messaging.TestWithInMemoryNetwork
|
||||
import core.node.services.*
|
||||
import core.protocols.ProtocolLogic
|
||||
import core.serialization.serialize
|
||||
|
@ -41,11 +41,19 @@ class NodeInterestRatesTest {
|
||||
|
||||
@Test fun `query with one success and one missing`() {
|
||||
val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-19 1M")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-15 1M")
|
||||
val e = assertFailsWith<NodeInterestRates.UnknownFix> { service.query(listOf(q1, q2)) }
|
||||
assertEquals(e.fix, q2)
|
||||
}
|
||||
|
||||
@Test fun `query successfully with one date beyond`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-19 1M")
|
||||
val res = service.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals("0.678".bd, res[0].value)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
|
||||
@Test fun `empty query`() {
|
||||
assertFailsWith<IllegalArgumentException> { service.query(emptyList()) }
|
||||
}
|
||||
|
@ -38,8 +38,6 @@ object TestUtils {
|
||||
val keypair = generateKeyPair()
|
||||
val keypair2 = generateKeyPair()
|
||||
val keypair3 = generateKeyPair()
|
||||
val keypair4 = generateKeyPair()
|
||||
val keypair5 = generateKeyPair()
|
||||
}
|
||||
// A dummy time at which we will be pretending test transactions are created.
|
||||
val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
@ -51,14 +49,6 @@ val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public
|
||||
val MINI_CORP_KEY = TestUtils.keypair2
|
||||
val MINI_CORP_PUBKEY = MINI_CORP_KEY.public
|
||||
|
||||
// TODO remove once mock API is retired
|
||||
val EXCALIBUR_BANK_KEY = TestUtils.keypair4
|
||||
val EXCALIBUR_BANK_PUBKEY = EXCALIBUR_BANK_KEY.public
|
||||
|
||||
// TODO remove once mock API is retired
|
||||
val A_N_OTHER_BANK_KEY = TestUtils.keypair5
|
||||
val A_N_OTHER_BANK_PUBKEY = A_N_OTHER_BANK_KEY.public
|
||||
|
||||
val ORACLE_KEY = TestUtils.keypair3
|
||||
val ORACLE_PUBKEY = ORACLE_KEY.public
|
||||
|
||||
@ -74,17 +64,11 @@ val BOB = BOB_KEY.public
|
||||
val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY)
|
||||
val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY)
|
||||
|
||||
// TODO remove once mock API is retired
|
||||
val EXCALIBUR_BANK = Party("Excalibur", EXCALIBUR_BANK_PUBKEY)
|
||||
val A_N_OTHER_BANK = Party("ANOther",A_N_OTHER_BANK_PUBKEY)
|
||||
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, EXCALIBUR_BANK_KEY, A_N_OTHER_BANK_KEY, DummyTimestampingAuthority.key)
|
||||
val ALL_TEST_KEYS = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DummyTimestampingAuthority.key)
|
||||
|
||||
val TEST_KEYS_TO_CORP_MAP: Map<PublicKey, Party> = mapOf(
|
||||
MEGA_CORP_PUBKEY to MEGA_CORP,
|
||||
MINI_CORP_PUBKEY to MINI_CORP,
|
||||
EXCALIBUR_BANK_PUBKEY to EXCALIBUR_BANK,
|
||||
A_N_OTHER_BANK_PUBKEY to A_N_OTHER_BANK,
|
||||
DUMMY_TIMESTAMPER.identity.owningKey to DUMMY_TIMESTAMPER.identity
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user