diff --git a/build.gradle b/build.gradle index e3a664f5de..63b95c82ca 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { // // TODO: Sort this alphabetically. ext.kotlin_version = constants.getProperty("kotlinVersion") - ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved. + ext.quasar_version = '0.7.9' // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 // TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3 @@ -42,7 +42,6 @@ buildscript { ext.hibernate_version = '5.2.6.Final' ext.h2_version = '1.4.194' ext.rxjava_version = '1.2.4' - ext.requery_version = '1.3.1' ext.dokka_version = '0.9.14' ext.eddsa_version = '0.2.0' @@ -128,6 +127,13 @@ allprojects { tasks.withType(Test) { // Prevent the project from creating temporary files outside of the build directory. systemProperties['java.io.tmpdir'] = buildDir + + // Ensures that "net.corda.testing.amqp.enable" is passed correctly from Gradle command line + // down to JVM executing unit test. It looks like we are running unit tests in the forked mode + // and all the "-D" parameters passed to Gradle not making it to unit test level + // TODO: Remove once we fully switched to AMQP + final AMQP_ENABLE_PROP_NAME = "net.corda.testing.amqp.enable" + systemProperty(AMQP_ENABLE_PROP_NAME, System.getProperty(AMQP_ENABLE_PROP_NAME)) } group 'com.r3.corda.enterprise' @@ -250,7 +256,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] + publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' @@ -285,7 +291,7 @@ artifactory { password = System.getenv('CORDA_ARTIFACTORY_PASSWORD') } defaults { - publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver') + publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver') } } } diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index f591484aa0..4e6c9b1f69 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -5,7 +5,6 @@ apply plugin: 'com.jfrog.artifactory' dependencies { compile project(':core') - compile project(':finance') testCompile project(':test-utils') compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" @@ -17,6 +16,7 @@ dependencies { compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jackson_version" // This adds support for java.time types. compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version" + compile "com.google.guava:guava:$guava_version" testCompile project(path: ':core', configuration: 'testArtifacts') testCompile "junit:junit:$junit_version" diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index ee8333f06e..cf2ed02f52 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -5,11 +5,9 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.* import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.deser.std.NumberDeserializers -import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule -import net.corda.contracts.BusinessCalendar import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef @@ -33,7 +31,6 @@ 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.* /** @@ -83,8 +80,6 @@ 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 addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer) @@ -276,36 +271,6 @@ object JacksonSupport { } } - data class BusinessCalendarWrapper(val holidayDates: List) { - fun toCalendar() = BusinessCalendar(holidayDates) - } - - object CalendarSerializer : JsonSerializer() { - 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() { - override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar { - return try { - 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}") - } - } - } - object PublicKeySerializer : JsonSerializer() { override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) { check(obj.params == Crypto.EDDSA_ED25519_SHA512.algSpec) diff --git a/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt index a92edb7aea..13acd147ab 100644 --- a/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/jackson/JacksonSupportTest.kt @@ -2,7 +2,7 @@ package net.corda.jackson import com.fasterxml.jackson.databind.SerializationFeature import net.corda.core.contracts.Amount -import net.corda.core.contracts.USD +import net.corda.finance.USD import net.corda.core.crypto.Crypto import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index 9c70ee7a04..8b8c677e4b 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -53,7 +53,7 @@ dependencies { } task integrationTest(type: Test) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 94662d7378..83453feddd 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -2,15 +2,13 @@ package net.corda.client.jfx import net.corda.client.jfx.model.NodeMonitorModel import net.corda.client.jfx.model.ProgressTrackingEvent -import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.contracts.Amount import net.corda.core.contracts.ContractState -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.USD import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowInitiator import net.corda.core.flows.StateMachineRunId +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.messaging.StateMachineUpdate @@ -22,11 +20,13 @@ import net.corda.core.node.services.Vault import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.CashExitFlow -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow +import net.corda.finance.DOLLARS +import net.corda.finance.USD +import net.corda.finance.flows.CashExitFlow +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.* @@ -108,13 +108,10 @@ class NodeMonitorModelTest : DriverBasedTest() { @Test fun `cash issue works end to end`() { - val anonymous = false rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), - aliceNode.legalIdentity, - notaryNode.notaryIdentity, - anonymous + notaryNode.notaryIdentity ) vaultUpdates.expectEvents(isStrict = false) { @@ -136,7 +133,7 @@ class NodeMonitorModelTest : DriverBasedTest() { @Test fun `cash issue and move`() { val anonymous = false - rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() + rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), notaryNode.notaryIdentity).returnValue.getOrThrow() rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow() var issueSmId: StateMachineRunId? = null diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt index 4944ca31b9..6a81eb0dc9 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/ContractStateModel.kt @@ -4,10 +4,10 @@ import javafx.collections.FXCollections import javafx.collections.ObservableList import net.corda.client.jfx.utils.fold import net.corda.client.jfx.utils.map -import net.corda.contracts.asset.Cash import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.node.services.Vault +import net.corda.finance.contracts.asset.Cash import rx.Observable data class Diff( diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index 6bd7b68e7c..1955dff5d2 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -1,12 +1,14 @@ package net.corda.client.mock import net.corda.core.contracts.Amount -import net.corda.core.contracts.GBP -import net.corda.core.contracts.USD import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes -import net.corda.flows.CashFlowCommand +import net.corda.finance.GBP +import net.corda.finance.USD import java.util.* +import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest +import net.corda.finance.flows.CashExitFlow.ExitRequest +import net.corda.finance.flows.CashPaymentFlow.PaymentRequest /** * [Generator]s for incoming/outgoing cash flow events between parties. It doesn't necessarily generate correct events! @@ -26,16 +28,16 @@ open class EventGenerator(val parties: List, val currencies: List addToMap(ccy, amount) - CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary, anonymous = true) + IssueAndPaymentRequest(Amount(amount, ccy), issueRef, to, notary, anonymous = true) } protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy -> addToMap(ccy, -amount) - CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef) + ExitRequest(Amount(amount, ccy), issueRef) } open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> - CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true) + PaymentRequest(Amount(amountIssued, currency), recipient, anonymous = true) } open val issuerGenerator = Generator.frequency(listOf( @@ -54,28 +56,28 @@ class ErrorFlowsEventGenerator(parties: List, currencies: List, EXIT_ERROR } - val errorGenerator = Generator.pickOne(IssuerEvents.values().toList()) + private val errorGenerator = Generator.pickOne(IssuerEvents.values().toList()) - val errorExitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator, errorGenerator) { amount, issueRef, ccy, errorType -> + private val errorExitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator, errorGenerator) { amount, issueRef, ccy, errorType -> when (errorType) { IssuerEvents.NORMAL_EXIT -> { println("Normal exit") if (currencyMap[ccy]!! <= amount) addToMap(ccy, -amount) - CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care. + ExitRequest(Amount(amount, ccy), issueRef) // It may fail at the beginning, but we don't care. } IssuerEvents.EXIT_ERROR -> { println("Exit error") - CashFlowCommand.ExitCash(Amount(currencyMap[ccy]!! * 2, ccy), issueRef) + ExitRequest(Amount(currencyMap[ccy]!! * 2, ccy), issueRef) } } } - val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> - CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true) + private val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> + PaymentRequest(Amount(amountIssued, currency), recipient, anonymous = true) } - val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency -> - CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true) + private val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency -> + PaymentRequest(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true) } override val moveCashGenerator = Generator.frequency(listOf( diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 61159ac74d..70ebc5ed57 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -50,6 +50,9 @@ processSmokeTestResources { from(project(':node:capsule').tasks['buildCordaJAR']) { rename 'corda-(.*)', 'corda.jar' } + from(project(':finance').tasks['jar']) { + rename 'finance-(.*)', 'finance.jar' + } } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in @@ -78,12 +81,12 @@ dependencies { } task integrationTest(type: Test) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } task smokeTest(type: Test) { - testClassesDir = sourceSets.smokeTest.output.classesDir + testClassesDirs = sourceSets.smokeTest.output.classesDirs classpath = sourceSets.smokeTest.runtimeClasspath } diff --git a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java index 9f38c17316..5fd5ec83b3 100644 --- a/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java +++ b/client/rpc/src/integration-test/java/net/corda/client/rpc/CordaRPCJavaClientTest.java @@ -1,15 +1,15 @@ package net.corda.client.rpc; -import net.corda.core.concurrent.CordaFuture; import net.corda.client.rpc.internal.RPCClient; +import net.corda.core.concurrent.CordaFuture; import net.corda.core.contracts.Amount; import net.corda.core.messaging.CordaRPCOps; import net.corda.core.messaging.FlowHandle; import net.corda.core.node.services.ServiceInfo; import net.corda.core.utilities.OpaqueBytes; -import net.corda.flows.AbstractCashFlow; -import net.corda.flows.CashIssueFlow; -import net.corda.flows.CashPaymentFlow; +import net.corda.finance.flows.AbstractCashFlow; +import net.corda.finance.flows.CashIssueFlow; +import net.corda.finance.flows.CashPaymentFlow; import net.corda.node.internal.Node; import net.corda.node.services.transactions.ValidatingNotaryService; import net.corda.nodeapi.User; @@ -22,10 +22,14 @@ import java.io.IOException; import java.util.*; import java.util.concurrent.ExecutionException; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static kotlin.test.AssertionsKt.assertEquals; import static net.corda.client.rpc.CordaRPCClientConfiguration.getDefault; -import static net.corda.contracts.GetBalances.getCashBalance; -import static net.corda.node.services.RPCUserServiceKt.startFlowPermission; +import static net.corda.finance.CurrencyUtils.DOLLARS; +import static net.corda.finance.contracts.GetBalances.getCashBalance; +import static net.corda.node.services.FlowPermissions.startFlowPermission; import static net.corda.testing.TestConstants.getALICE; public class CordaRPCJavaClientTest extends NodeBasedTest { @@ -45,10 +49,10 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { @Before public void setUp() throws ExecutionException, InterruptedException { - Set services = new HashSet<>(Collections.singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null))); - CordaFuture nodeFuture = startNode(getALICE().getName(), 1, services, Arrays.asList(rpcUser), Collections.emptyMap()); + Set services = new HashSet<>(singletonList(new ServiceInfo(ValidatingNotaryService.Companion.getType(), null))); + CordaFuture nodeFuture = startNode(getALICE().getName(), 1, services, singletonList(rpcUser), emptyMap()); node = nodeFuture.get(); - client = new CordaRPCClient(node.getConfiguration().getRpcAddress(), null, getDefault(), false); + client = new CordaRPCClient(requireNonNull(node.getConfiguration().getRpcAddress()), null, getDefault(), false); } @After @@ -65,17 +69,15 @@ public class CordaRPCJavaClientTest extends NodeBasedTest { public void testCashBalances() throws NoSuchFieldException, ExecutionException, InterruptedException { login(rpcUser.getUsername(), rpcUser.getPassword()); - Amount dollars123 = new Amount<>(123, Currency.getInstance("USD")); - FlowHandle flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, - dollars123, OpaqueBytes.of("1".getBytes()), - node.info.getLegalIdentity(), node.info.getLegalIdentity()); + DOLLARS(123), OpaqueBytes.of("1".getBytes()), + node.info.getLegalIdentity()); System.out.println("Started issuing cash, waiting on result"); flowHandle.getReturnValue().get(); Amount balance = getCashBalance(rpcProxy, Currency.getInstance("USD")); System.out.print("Balance: " + balance + "\n"); - assertEquals(dollars123, balance, "matching"); + assertEquals(DOLLARS(123), balance, "matching"); } } diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt index 19e5fdc50e..6c8a7d7a8c 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/CordaRPCClientTest.kt @@ -1,20 +1,20 @@ package net.corda.client.rpc -import net.corda.contracts.getCashBalance -import net.corda.contracts.getCashBalances -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.USD import net.corda.core.crypto.random63BitValue import net.corda.core.flows.FlowInitiator import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.CashException -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow +import net.corda.finance.DOLLARS +import net.corda.finance.USD +import net.corda.finance.contracts.getCashBalance +import net.corda.finance.contracts.getCashBalances +import net.corda.finance.flows.CashException +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.Node -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.ALICE @@ -77,9 +77,9 @@ class CordaRPCClientTest : NodeBasedTest() { login(rpcUser.username, rpcUser.password) println("Creating proxy") println("Starting flow") - val flowHandle = connection!!.proxy.startTrackedFlow( - ::CashIssueFlow, - 20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity, node.info.legalIdentity) + val flowHandle = connection!!.proxy.startTrackedFlow(::CashIssueFlow, + 20.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity + ) println("Started flow, waiting on result") flowHandle.progress.subscribe { println("PROGRESS $it") @@ -113,8 +113,7 @@ class CordaRPCClientTest : NodeBasedTest() { assertTrue(startCash.isEmpty(), "Should not start with any cash") val flowHandle = proxy.startFlow(::CashIssueFlow, - 123.DOLLARS, OpaqueBytes.of(0), - node.info.legalIdentity, node.info.legalIdentity + 123.DOLLARS, OpaqueBytes.of(0), node.info.legalIdentity ) println("Started issuing cash, waiting on result") flowHandle.returnValue.get() @@ -140,14 +139,16 @@ class CordaRPCClientTest : NodeBasedTest() { } } val nodeIdentity = node.info.legalIdentity - node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity, nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow() + node.services.startFlow(CashIssueFlow(2000.DOLLARS, OpaqueBytes.of(0), nodeIdentity), FlowInitiator.Shell).resultFuture.getOrThrow() proxy.startFlow(::CashIssueFlow, - 123.DOLLARS, OpaqueBytes.of(0), - nodeIdentity, nodeIdentity + 123.DOLLARS, + OpaqueBytes.of(0), + nodeIdentity ).returnValue.getOrThrow() proxy.startFlowDynamic(CashIssueFlow::class.java, - 1000.DOLLARS, OpaqueBytes.of(0), - nodeIdentity, nodeIdentity).returnValue.getOrThrow() + 1000.DOLLARS, + OpaqueBytes.of(0), + nodeIdentity).returnValue.getOrThrow() assertEquals(2, countRpcFlows) assertEquals(1, countShellFlows) } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index 3ec8268c3e..a0a4a7ea1f 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -71,7 +71,7 @@ class CordaRPCClient( fun initialiseSerialization() { try { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { - registerScheme(KryoClientSerializationScheme(this)) + registerScheme(KryoClientSerializationScheme()) registerScheme(AMQPClientSerializationScheme()) } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 94fa65a018..cca836d858 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -5,10 +5,10 @@ import net.corda.core.internal.logElapsedTime import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationDefaults -import net.corda.core.utilities.minutes -import net.corda.core.utilities.seconds import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.minutes +import net.corda.core.utilities.seconds import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.RPCApi @@ -110,6 +110,21 @@ class RPCClient( val proxy: I /** The RPC protocol version reported by the server */ val serverProtocolVersion: Int + + /** + * Closes this client without notifying the server. + * The server will eventually clear out the RPC message queue and disconnect subscribed observers, + * but this may take longer than desired, so to conserve resources you should normally use [notifyServerAndClose]. + * This method is helpful when the node may be shutting down or + * have already shut down and you don't want to block waiting for it to come back. + */ + fun forceClose() + + /** + * Closes this client gracefully by sending a notification to the server, so it can immediately clean up resources. + * If the server is not available this method may block for a short period until it's clear the server is not coming back. + */ + fun notifyServerAndClose() } /** @@ -168,13 +183,30 @@ class RPCClient( object : RPCConnection { override val proxy = ops override val serverProtocolVersion = serverProtocolVersion - override fun close() { - proxyHandler.close() + + private fun close(notify: Boolean) { + if (notify) { + proxyHandler.notifyServerAndClose() + } else { + proxyHandler.forceClose() + } serverLocator.close() } + + override fun notifyServerAndClose() { + close(true) + } + + override fun forceClose() { + close(false) + } + + override fun close() { + close(true) + } } } catch (exception: Throwable) { - proxyHandler.close() + proxyHandler.notifyServerAndClose() serverLocator.close() throw exception } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index 47a20df007..27d8157dde 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -10,14 +10,17 @@ import com.google.common.cache.RemovalCause import com.google.common.cache.RemovalListener import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.ThreadFactoryBuilder -import net.corda.core.internal.ThreadBox import net.corda.core.crypto.random63BitValue import net.corda.core.internal.LazyPool import net.corda.core.internal.LazyStickyPool import net.corda.core.internal.LifeCycle +import net.corda.core.internal.ThreadBox import net.corda.core.messaging.RPCOps import net.corda.core.serialization.SerializationContext -import net.corda.core.utilities.* +import net.corda.core.utilities.Try +import net.corda.core.utilities.debug +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor import net.corda.nodeapi.* import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration import org.apache.activemq.artemis.api.core.SimpleString @@ -113,7 +116,8 @@ class RPCClientProxyHandler( private fun createRpcObservableMap(): RpcObservableMap { val onObservableRemove = RemovalListener>> { - val rpcCallSite = callSiteMap?.remove(it.key.toLong) + val observableId = it.key!! + val rpcCallSite = callSiteMap?.remove(observableId.toLong) if (it.cause == RemovalCause.COLLECTED) { log.warn(listOf( "A hot observable returned from an RPC was never subscribed to.", @@ -123,7 +127,7 @@ class RPCClientProxyHandler( "will appear less frequently in future versions of the platform and you can ignore it", "if you want to.").joinToString(" "), rpcCallSite) } - observablesToReap.locked { observables.add(it.key) } + observablesToReap.locked { observables.add(observableId) } } return CacheBuilder.newBuilder(). weakValues(). @@ -159,7 +163,7 @@ class RPCClientProxyHandler( ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build() ) reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( - this::reapObservables, + this::reapObservablesAndNotify, rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(), TimeUnit.MILLISECONDS @@ -268,13 +272,36 @@ class RPCClientProxyHandler( } /** - * Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors. + * Closes this handler without notifying observables. + * This method clears up only local resources and as such does not block on any network resources. */ - fun close() { + fun forceClose() { + close(false) + } + + /** + * Closes this handler and sends notifications to all observables, so it can immediately clean up resources. + * Notifications sent to observables are to be acknowledged, therefore this call blocks until all acknowledgements are received. + * If this is not convenient see the [forceClose] method. + * If an observable is not accessible this method may block for a duration of the message broker timeout. + */ + fun notifyServerAndClose() { + close(true) + } + + /** + * Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors. + * When observables are to be notified (i.e. the [notify] parameter is true), + * the method blocks until all the messages are acknowledged by the observables. + * Note: If any of the observables is inaccessible, the method blocks for the duration of the timeout set on the message broker. + * + * @param notify whether to notify observables or not. + */ + private fun close(notify: Boolean = true) { sessionAndConsumer?.sessionFactory?.close() reaperScheduledFuture?.cancel(false) observableContext.observableMap.invalidateAll() - reapObservables() + reapObservables(notify) reaperExecutor?.shutdownNow() sessionAndProducerPool.close().forEach { it.sessionFactory.close() @@ -315,8 +342,11 @@ class RPCClientProxyHandler( lifeCycle.transition(State.SERVER_VERSION_NOT_SET, State.STARTED) } - private fun reapObservables() { + private fun reapObservablesAndNotify() = reapObservables() + + private fun reapObservables(notify: Boolean = true) { observableContext.observableMap.cleanUp() + if (!notify) return val observableIds = observablesToReap.locked { if (observables.isNotEmpty()) { val temporary = observables diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt index 0bb26b93fb..817d741c64 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/serialization/SerializationScheme.kt @@ -3,21 +3,20 @@ package net.corda.client.rpc.serialization import com.esotericsoftware.kryo.pool.KryoPool import net.corda.client.rpc.internal.RpcClientObservableSerializer import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.ByteSequence import net.corda.nodeapi.RPCKryo import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1 -class KryoClientSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) { +class KryoClientSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return byteSequence == KryoHeaderV0_1 && (target == SerializationContext.UseCase.RPCClient || target == SerializationContext.UseCase.P2P) } override fun rpcClientKryoPool(context: SerializationContext): KryoPool { return KryoPool.Builder { - DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, serializationFactory, context)).apply { classLoader = context.deserializationClassLoader } + DefaultKryoCustomizer.customize(RPCKryo(RpcClientObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader } }.build() } diff --git a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java index 6c18e31053..2cd3ad6628 100644 --- a/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java +++ b/client/rpc/src/smoke-test/java/net/corda/java/rpc/StandaloneCordaRPCJavaClientTest.java @@ -3,13 +3,11 @@ package net.corda.java.rpc; import net.corda.client.rpc.CordaRPCConnection; import net.corda.core.contracts.Amount; import net.corda.core.messaging.CordaRPCOps; -import net.corda.core.messaging.DataFeed; import net.corda.core.messaging.FlowHandle; import net.corda.core.node.NodeInfo; -import net.corda.core.node.services.NetworkMapCache; import net.corda.core.utilities.OpaqueBytes; -import net.corda.flows.AbstractCashFlow; -import net.corda.flows.CashIssueFlow; +import net.corda.finance.flows.AbstractCashFlow; +import net.corda.finance.flows.CashIssueFlow; import net.corda.nodeapi.User; import net.corda.smoketesting.NodeConfig; import net.corda.smoketesting.NodeProcess; @@ -18,12 +16,18 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import static kotlin.test.AssertionsKt.assertEquals; -import static net.corda.contracts.GetBalances.getCashBalance; +import static kotlin.test.AssertionsKt.fail; +import static net.corda.finance.contracts.GetBalances.getCashBalance; public class StandaloneCordaRPCJavaClientTest { private List perms = Collections.singletonList("ALL"); @@ -32,6 +36,7 @@ public class StandaloneCordaRPCJavaClientTest { private AtomicInteger port = new AtomicInteger(15000); + private NodeProcess.Factory factory; private NodeProcess notary; private CordaRPCOps rpcProxy; private CordaRPCConnection connection; @@ -49,7 +54,9 @@ public class StandaloneCordaRPCJavaClientTest { @Before public void setUp() { - notary = new NodeProcess.Factory().create(notaryConfig); + factory = new NodeProcess.Factory(); + copyFinanceCordapp(); + notary = factory.create(notaryConfig); connection = notary.connect(); rpcProxy = connection.getProxy(); notaryNode = fetchNotaryIdentity(); @@ -60,7 +67,31 @@ public class StandaloneCordaRPCJavaClientTest { try { connection.close(); } finally { - notary.close(); + if(notary != null) { + notary.close(); + } + } + } + + private void copyFinanceCordapp() { + Path pluginsDir = (factory.baseDirectory(notaryConfig).resolve("plugins")); + try { + Files.createDirectories(pluginsDir); + } catch (IOException ex) { + fail("Failed to create directories"); + } + try (Stream paths = Files.walk(Paths.get("build", "resources", "smokeTest"))) { + paths.forEach(file -> { + if (file.toString().contains("corda-finance")) { + try { + Files.copy(file, pluginsDir.resolve(file.getFileName())); + } catch (IOException ex) { + fail("Failed to copy finance jar"); + } + } + }); + } catch (IOException e) { + fail("Failed to walk files"); } } @@ -75,7 +106,7 @@ public class StandaloneCordaRPCJavaClientTest { FlowHandle flowHandle = rpcProxy.startFlowDynamic(CashIssueFlow.class, dollars123, OpaqueBytes.of("1".getBytes()), - notaryNode.getLegalIdentity(), notaryNode.getLegalIdentity()); + notaryNode.getLegalIdentity()); System.out.println("Started issuing cash, waiting on result"); flowHandle.getReturnValue().get(); diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt index 064db6fc7d..a8c670dc2a 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -3,13 +3,8 @@ package net.corda.kotlin.rpc import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import net.corda.client.rpc.CordaRPCConnection -import net.corda.client.rpc.notUsed -import net.corda.contracts.asset.Cash -import net.corda.contracts.getCashBalance -import net.corda.contracts.getCashBalances -import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash -import net.corda.core.internal.InputStreamAndHash +import net.corda.core.internal.* import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.Vault @@ -18,8 +13,15 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow +import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS +import net.corda.finance.SWISS_FRANCS +import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.getCashBalance +import net.corda.finance.contracts.getCashBalances +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.nodeapi.User import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeProcess @@ -30,8 +32,11 @@ import org.junit.Before import org.junit.Test import java.io.FilterInputStream import java.io.InputStream +import java.nio.file.Paths import java.util.* +import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger +import kotlin.streams.toList import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotEquals @@ -41,11 +46,12 @@ class StandaloneCordaRPClientTest { private companion object { val log = loggerFor() val user = User("user1", "test", permissions = setOf("ALL")) - val port = AtomicInteger(15000) + val port = AtomicInteger(15200) const val attachmentSize = 2116 val timeout = 60.seconds } + private lateinit var factory: NodeProcess.Factory private lateinit var notary: NodeProcess private lateinit var rpcProxy: CordaRPCOps private lateinit var connection: CordaRPCConnection @@ -62,7 +68,9 @@ class StandaloneCordaRPClientTest { @Before fun setUp() { - notary = NodeProcess.Factory().create(notaryConfig) + factory = NodeProcess.Factory() + copyFinanceCordapp() + notary = factory.create(notaryConfig) connection = notary.connect() rpcProxy = connection.proxy notaryNode = fetchNotaryIdentity() @@ -77,6 +85,15 @@ class StandaloneCordaRPClientTest { } } + private fun copyFinanceCordapp() { + val pluginsDir = (factory.baseDirectory(notaryConfig) / "plugins").createDirectories() + // Find the finance jar file for the smoke tests of this module + val financeJar = Paths.get("build", "resources", "smokeTest").list { + it.filter { "corda-finance" in it.toString() }.toList().single() + } + financeJar.copyToDirectory(pluginsDir) + } + @Test fun `test attachments`() { val attachment = InputStreamAndHash.createInMemoryTestZip(attachmentSize, 1) @@ -93,7 +110,7 @@ class StandaloneCordaRPClientTest { @Test fun `test starting flow`() { - rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) } @@ -101,13 +118,16 @@ class StandaloneCordaRPClientTest { fun `test starting tracked flow`() { var trackCount = 0 val handle = rpcProxy.startTrackedFlow( - ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity + ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.notaryIdentity ) + val updateLatch = CountDownLatch(1) handle.progress.subscribe { msg -> log.info("Flow>> $msg") ++trackCount + updateLatch.countDown() } handle.returnValue.getOrThrow(timeout) + updateLatch.await() assertNotEquals(0, trackCount) } @@ -121,17 +141,20 @@ class StandaloneCordaRPClientTest { val (stateMachines, updates) = rpcProxy.stateMachinesFeed() assertEquals(0, stateMachines.size) + val updateLatch = CountDownLatch(1) val updateCount = AtomicInteger(0) updates.subscribe { update -> if (update is StateMachineUpdate.Added) { log.info("StateMachine>> Id=${update.id}") updateCount.incrementAndGet() + updateLatch.countDown() } } // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) + updateLatch.await() assertEquals(1, updateCount.get()) } @@ -140,16 +163,16 @@ class StandaloneCordaRPClientTest { val (vault, vaultUpdates) = rpcProxy.vaultTrackBy(paging = PageSpecification(DEFAULT_PAGE_NUM)) assertEquals(0, vault.totalStatesAvailable) - val updateCount = AtomicInteger(0) + val updateLatch = CountDownLatch(1) vaultUpdates.subscribe { update -> log.info("Vault>> FlowId=${update.flowId}") - updateCount.incrementAndGet() + updateLatch.countDown() } // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) - assertNotEquals(0, updateCount.get()) + updateLatch.await() // Check that this cash exists in the vault val cashBalance = rpcProxy.getCashBalances() @@ -161,7 +184,7 @@ class StandaloneCordaRPClientTest { @Test fun `test vault query by`() { // Now issue some cash - rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity) + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.notaryIdentity) .returnValue.getOrThrow(timeout) val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -187,12 +210,10 @@ class StandaloneCordaRPClientTest { @Test fun `test cash balances`() { val startCash = rpcProxy.getCashBalances() + println(startCash) assertTrue(startCash.isEmpty(), "Should not start with any cash") - val flowHandle = rpcProxy.startFlow(::CashIssueFlow, - 629.DOLLARS, OpaqueBytes.of(0), - notaryNode.legalIdentity, notaryNode.legalIdentity - ) + val flowHandle = rpcProxy.startFlow(::CashIssueFlow, 629.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity) println("Started issuing cash, waiting on result") flowHandle.returnValue.get() diff --git a/constants.properties b/constants.properties index 4567dcffbb..f8c76f96a6 100644 --- a/constants.properties +++ b/constants.properties @@ -1,5 +1,5 @@ -gradlePluginsVersion=0.13.6 -kotlinVersion=1.1.1 +gradlePluginsVersion=0.15.1 +kotlinVersion=1.1.4 guavaVersion=21.0 bouncycastleVersion=1.57 -typesafeConfigVersion=1.3.1 +typesafeConfigVersion=1.3.1 \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index edf8fb1037..ca33b3b725 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -42,7 +42,7 @@ dependencies { testCompile "org.assertj:assertj-core:${assertj_version}" // Guava: Google utilities library. - compile "com.google.guava:guava:$guava_version" + testCompile "com.google.guava:guava:$guava_version" // RxJava: observable streams of events. compile "io.reactivex:rxjava:$rxjava_version" @@ -63,12 +63,6 @@ dependencies { // JPA 2.1 annotations. compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final" - - // Requery: SQL based query & persistence for Kotlin - compile "io.requery:requery-kotlin:$requery_version" - - // For AMQP serialisation. - compile "org.apache.qpid:proton-j:0.19.0" } configurations { diff --git a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt index e08ddce671..6de842ff63 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Amount.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -1,9 +1,9 @@ package net.corda.core.contracts import net.corda.core.crypto.composite.CompositeKey -import net.corda.core.utilities.exactAdd import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.exactAdd import java.math.BigDecimal import java.math.RoundingMode import java.util.* @@ -13,6 +13,7 @@ import java.util.* * indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects. */ interface TokenizableAssetInfo { + /** The nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. */ val displayTokenSize: BigDecimal } @@ -28,16 +29,14 @@ interface TokenizableAssetInfo { * multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer * overflow. * - * @param quantity the number of tokens as a Long value. - * @param displayTokenSize the nominal display unit size of a single token, - * potentially with trailing decimal display places if the scale parameter is non-zero. - * @param T the type of the token, for example [Currency]. - * T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required. - * - * TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application. + * @property quantity the number of tokens as a Long value. + * @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. + * @property token an instance of type T, usually a singleton. + * @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required. */ @CordaSerializable data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable> { + // TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application. companion object { /** * Build an Amount from a decimal representation. For example, with an input of "12.34 GBP", @@ -73,6 +72,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * For other possible token types the asset token should implement TokenizableAssetInfo to * correctly report the designed nominal amount. */ + @JvmStatic fun getDisplayTokenSize(token: Any): BigDecimal { if (token is TokenizableAssetInfo) { return token.displayTokenSize @@ -86,14 +86,39 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, return BigDecimal.ONE } + /** + * If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if + * any of the token types are mismatched; if the iterator yields no elements, return null. + */ + @JvmStatic + fun Iterable>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow() + + /** + * Sums the amounts yielded by the given iterable, throwing an [IllegalArgumentException] if any of the token + * types are mismatched. + */ + @JvmStatic + fun Iterable>.sumOrThrow() = reduce { left, right -> left + right } + + /** + * If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if + * any of the token types are mismatched; if the iterator yields no elements, return a zero amount of the given + * token type. + */ + @JvmStatic + fun Iterable>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token) + private val currencySymbols: Map = mapOf( - "$" to USD, - "£" to GBP, - "€" to EUR, - "¥" to JPY, - "₽" to RUB + "$" to Currency.getInstance("USD"), + "£" to Currency.getInstance("GBP"), + "€" to Currency.getInstance("EUR"), + "¥" to Currency.getInstance("JPY"), + "₽" to Currency.getInstance("RUB") ) - private val currencyCodes: Map by lazy { Currency.getAvailableCurrencies().map { it.currencyCode to it }.toMap() } + + private val currencyCodes: Map by lazy { + Currency.getAvailableCurrencies().associateBy { it.currencyCode } + } /** * Returns an amount that is equal to the given currency amount in text. Examples of what is supported: @@ -120,6 +145,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * * @throws IllegalArgumentException if the input string was not understood. */ + @JvmStatic fun parseCurrency(input: String): Amount { val i = input.filter { it != ',' } try { @@ -127,7 +153,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, for ((symbol, currency) in currencySymbols) { if (i.startsWith(symbol)) { val rest = i.substring(symbol.length) - return fromDecimal(BigDecimal(rest), currency) + return Amount.fromDecimal(BigDecimal(rest), currency) } } // Now check the codes at the end. @@ -136,7 +162,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, val (rest, code) = split for ((cc, currency) in currencyCodes) { if (cc == code) { - return fromDecimal(BigDecimal(rest), currency) + return Amount.fromDecimal(BigDecimal(rest), currency) } } } @@ -166,8 +192,9 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, /** * A checked addition operator is supported to simplify aggregation of Amounts. + * Mixing non-identical token types will throw [IllegalArgumentException]. + * * @throws ArithmeticException if there is overflow of Amount tokens during the summation - * Mixing non-identical token types will throw [IllegalArgumentException] */ operator fun plus(other: Amount): Amount { checkToken(other) @@ -177,8 +204,9 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, /** * A checked addition operator is supported to simplify netting of Amounts. * If this leads to the Amount going negative this will throw [IllegalArgumentException]. + * Mixing non-identical token types will throw [IllegalArgumentException]. + * * @throws ArithmeticException if there is Numeric underflow - * Mixing non-identical token types will throw [IllegalArgumentException] */ operator fun minus(other: Amount): Amount { checkToken(other) @@ -197,6 +225,11 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, */ operator fun times(other: Long): Amount = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token) + /** + * The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount. + * Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour. + * N.B. Division is not supported as fractional tokens are not representable by an Amount. + */ operator fun times(other: Int): Amount = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token) /** @@ -219,7 +252,7 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize, * which determines the size of a single token and controls the trailing decimal places via it's scale property. * - * @see Amount.Companion.fromDecimal + * @see Amount.fromDecimal */ fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize @@ -231,29 +264,27 @@ data class Amount(val quantity: Long, val displayTokenSize: BigDecimal, * The result of fromDecimal is used to control the numerical formatting and * the token specifier appended is taken from token.toString. * - * @see Amount.Companion.fromDecimal + * @see Amount.fromDecimal */ override fun toString(): String { return toDecimal().toPlainString() + " " + token } + /** @suppress */ override fun compareTo(other: Amount): Int { checkToken(other) return quantity.compareTo(other.quantity) } } - -fun Iterable>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow() -fun Iterable>.sumOrThrow() = reduce { left, right -> left + right } -fun Iterable>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token) - - /** * Simple data class to associate the origin, owner, or holder of a particular Amount object. - * @param source the holder of the Amount. - * @param amount the Amount of asset available. - * @param ref is an optional field used for housekeeping in the caller. + * + * @param P Any class type that can disambiguate where the amount came from. + * @param T The token type of the underlying [Amount]. + * @property source the holder of the Amount. + * @property amount the Amount of asset available. + * @property ref is an optional field used for housekeeping in the caller. * e.g. to point back at the original Vault state objects. * @see SourceAndAmount.apply which processes a list of SourceAndAmount objects * and calculates the resulting Amount distribution as a new list of SourceAndAmount objects. @@ -263,17 +294,17 @@ data class SourceAndAmount(val source: P, val amount: Amou /** * This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date. * - * @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then + * @property quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then * it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta) * at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens * and the destination will lose Math.abs(quantityDelta) tokens. * Where possible the source and destination should be coded to ensure a positive quantityDelta, * but in various scenarios it may be more consistent to allow positive and negative values. * For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination. - * @param token represents the type of asset token as would be used to construct Amount objects. - * @param source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive, + * @property token represents the type of asset token as would be used to construct Amount objects. + * @property source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive, * or the token sink if quantityDelta is negative. The type P should support value equality. - * @param destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive, + * @property destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive, * or the token source if quantityDelta is negative. The type P should support value equality. */ @CordaSerializable @@ -305,9 +336,7 @@ class AmountTransfer(val quantityDelta: Long, return AmountTransfer(deltaTokenCount, token, source, destination) } - /** - * Helper to make a zero size AmountTransfer - */ + /** Helper to make a zero size AmountTransfer. */ @JvmStatic fun zero(token: T, source: P, @@ -344,6 +373,7 @@ class AmountTransfer(val quantityDelta: Long, */ fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token) + /** @suppress */ fun copy(quantityDelta: Long = this.quantityDelta, token: T = this.token, source: P = this.source, @@ -373,7 +403,7 @@ class AmountTransfer(val quantityDelta: Long, } /** - * HashCode ensures that reversed source and destination equivalents will hash to the same value. + * This hash code function ensures that reversed source and destination equivalents will hash to the same value. */ override fun hashCode(): Int { var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values @@ -382,18 +412,20 @@ class AmountTransfer(val quantityDelta: Long, return result } + /** @suppress */ override fun toString(): String { return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token" } /** - * Novation is a common financial operation in which a bilateral exchange is modified so that the same - * relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house. + * Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. The net + * total exchange is the same as in the original input. Novation is a common financial operation in which a + * bilateral exchange is modified so that the same relative asset exchange happens, but with each party exchanging + * versus a central counterparty, or clearing house. * * @param centralParty The central party to face the exchange against. - * @return Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. - * The net total exchange is the same as in the original input. */ + @Suppress("UNUSED") fun novate(centralParty: P): List> = listOf(copy(destination = centralParty), copy(source = centralParty)) /** @@ -403,7 +435,7 @@ class AmountTransfer(val quantityDelta: Long, * @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange. * @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output. * i.e. To the new payment destination entry and to any residual change output. - * @return The returned list is a copy of the original list, except that funds needed to cover the exchange + * @return A copy of the original list, except that funds needed to cover the exchange * will have been removed and a new output and possibly residual amount entry will be added at the end of the list. * @throws ArithmeticException if there is underflow in the summations. */ diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index fd38e9aa99..c1d43020f3 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -4,7 +4,6 @@ package net.corda.core.contracts import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import java.math.BigDecimal import java.security.PublicKey import java.util.* @@ -12,41 +11,10 @@ import java.util.* * Defines a simple domain specific language for the specification of financial contracts. Currently covers: * * - Some utilities for working with commands. - * - Code for working with currencies. - * - An Amount type that represents a positive quantity of a specific currency. + * - An Amount type that represents a positive quantity of a specific token. * - A simple language extension for specifying requirements in English, along with logic to enforce them. - * - * TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354) */ -//// Currencies /////////////////////////////////////////////////////////////////////////////////////////////////////// - -fun currency(code: String) = Currency.getInstance(code)!! - -@JvmField val USD = currency("USD") -@JvmField val GBP = currency("GBP") -@JvmField val EUR = currency("EUR") -@JvmField val CHF = currency("CHF") -@JvmField val JPY = currency("JPY") -@JvmField val RUB = currency("RUB") - -fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) -fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) -fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) -fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) -fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) -fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) - -val Int.DOLLARS: Amount get() = DOLLARS(this) -val Double.DOLLARS: Amount get() = DOLLARS(this) -val Int.POUNDS: Amount get() = POUNDS(this) -val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) - -infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) -infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit)) - //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// object Requirements { diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index bf6d856d97..dea478ecc6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -14,49 +14,28 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException * crude are fungible and countable (oil from two small containers can be poured into one large * container), shares of the same class in a specific company are fungible and countable, and so on. * - * See [Cash] for an example contract that implements currency using state objects that implement + * An example usage would be a cash transaction contract that implements currency using state objects that implement * this interface. * * @param T a type that represents the asset in question. This should describe the basic type of the asset * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). */ interface FungibleAsset : OwnableState { + /** + * Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally + * anything else that's quantifiable with integer quantities. See [Issued] and [Amount] for more details. + */ val amount: Amount> + /** * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their * owner to sign, some (i.e. cash) also require the issuer. */ val exitKeys: Collection - /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: AbstractParty - fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset - - // Just for grouping - interface Commands : CommandData { - interface Move : MoveCommand, Commands - - /** - * Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction - * has a unique ID even when there are no inputs. - */ - interface Issue : IssueCommand, Commands - - /** - * A command stating that money has been withdrawn from the shared ledger and is now accounted for - * in some other way. - */ - interface Exit : Commands { - val amount: Amount> - } - } + /** + * Copies the underlying data structure, replacing the amount and owner fields with the new values and leaving the + * rest (exitKeys) alone. + */ + fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset } - -// Small DSL extensions. - -/** Sums the asset states in the list, returning null if there are none. */ -fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() - -/** Sums the asset states in the list, returning zero of the given token if there are none. */ -fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) - diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index f820cdd674..a877e98b9f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -128,10 +128,24 @@ infix fun T.`with notary`(newNotary: Party) = withNotary(new infix fun T.withNotary(newNotary: Party) = TransactionState(this, newNotary) /** - * Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's - * quantifiable with integer quantities. + * The [Issued] data class holds the details of an on ledger digital asset. + * In particular it gives the public credentials of the entity that created these digital tokens + * and the particular product represented. * - * @param P the type of product underlying the definition, for example [java.util.Currency]. + * @param P the class type of product underlying the definition, for example [java.util.Currency]. + * @property issuer The [AbstractParty] details of the entity which issued the asset + * and a reference blob, which can contain other details related to the token creation e.g. serial number, + * warehouse location, etc. + * The issuer is the gatekeeper for creating, or destroying the tokens on the digital ledger and + * only their [PrivateKey] signature can authorise transactions that do not conserve the total number + * of tokens on the ledger. + * Other identities may own the tokens, but they can only create transactions that conserve the total token count. + * Typically the issuer is also a well know organisation that can convert digital tokens to external assets + * and thus underwrites the digital tokens. + * Different issuer values may coexist for a particular product, but these cannot be merged. + * @property product The details of the specific product represented by these digital tokens. The value + * of product may differentiate different kinds of asset within the same logical class e.g the currency, or + * it may just be a type marker for a single custom asset. */ @CordaSerializable data class Issued(val issuer: PartyAndReference, val product: P) { @@ -156,15 +170,15 @@ data class CommandAndState(val command: CommandData, val ownableState: OwnableSt * A contract state that can have a single owner. */ interface OwnableState : ContractState { - /** There must be a MoveCommand signed by this key to claim the amount */ + /** There must be a MoveCommand signed by this key to claim the amount. */ val owner: AbstractParty - /** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */ + /** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone. */ fun withNewOwner(newOwner: AbstractParty): CommandAndState } // DOCEND 3 -/** Something which is scheduled to happen at a point in time */ +/** Something which is scheduled to happen at a point in time. */ interface Scheduled { val scheduledAt: Instant } @@ -284,20 +298,14 @@ data class Command(val value: T, val signers: List) override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}" } -/** A common issue command, to enforce that issue commands have a nonce value. */ -// TODO: Revisit use of nonce values - should this be part of the TX rather than the command perhaps? -interface IssueCommand : CommandData { - val nonce: Long -} - /** A common move command for contract states which can change owner. */ interface MoveCommand : CommandData { /** * Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in * order to settle an obligation contract's state object(s). */ - // TODO: Replace SecureHash here with a general contract constraints object - val contractHash: SecureHash? + // TODO: Replace Class here with a general contract constraints object + val contract: Class? } /** Indicates that this transaction replaces the inputs contract state to another contract state */ @@ -333,15 +341,14 @@ interface Contract { */ @Throws(IllegalArgumentException::class) fun verify(tx: LedgerTransaction) - - /** - * Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of - * the contract's contents). - */ - val legalContractReference: SecureHash } // DOCEND 5 +/** The annotated [Contract] implements the legal prose identified by the given URI. */ +@Target(AnnotationTarget.CLASS) +@MustBeDocumented +annotation class LegalProseReference(val uri: String) + /** * Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract. * @@ -427,7 +434,7 @@ fun JarInputStream.extractFile(path: String, outputTo: OutputStream) { * A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot * use brute force techniques and reveal the content of a Merkle-leaf hashed value. * Because this salt serves the role of the seed to compute nonces, its size and entropy should be equal to the - * underlying hash function used for Merkle tree generation, currently [SHA256], which has an output of 32 bytes. + * underlying hash function used for Merkle tree generation, currently [SecureHash.SHA256], which has an output of 32 bytes. * There are two constructors, one that generates a new 32-bytes random salt, and another that takes a [ByteArray] input. * The latter is required in cases where the salt value needs to be pre-generated (agreed between transacting parties), * but it is highlighted that one should always ensure it has sufficient entropy. diff --git a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt index cf679e8a7d..8a62f308c4 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/ContentSignerBuilder.kt @@ -13,7 +13,7 @@ import java.security.Signature * This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm. */ object ContentSignerBuilder { - fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner { + fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner { val sigAlgId = signatureScheme.signatureOID val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply { if (random != null) { diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index 6db86fb563..8b1d9fb542 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -70,6 +70,7 @@ object Crypto { * RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function. * Note: Recommended key size >= 3072 bits. */ + @JvmField val RSA_SHA256 = SignatureScheme( 1, "RSA_SHA256", @@ -84,6 +85,7 @@ object Crypto { ) /** ECDSA signature scheme using the secp256k1 Koblitz curve. */ + @JvmField val ECDSA_SECP256K1_SHA256 = SignatureScheme( 2, "ECDSA_SECP256K1_SHA256", @@ -98,6 +100,7 @@ object Crypto { ) /** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */ + @JvmField val ECDSA_SECP256R1_SHA256 = SignatureScheme( 3, "ECDSA_SECP256R1_SHA256", @@ -112,6 +115,7 @@ object Crypto { ) /** EdDSA signature scheme using the ed255519 twisted Edwards curve. */ + @JvmField val EDDSA_ED25519_SHA512 = SignatureScheme( 4, "EDDSA_ED25519_SHA512", @@ -131,7 +135,10 @@ object Crypto { * SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers * at the cost of larger key sizes and loss of compatibility. */ + @JvmField val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256)) + + @JvmField val SPHINCS256_SHA256 = SignatureScheme( 5, "SPHINCS-256_SHA512", @@ -146,13 +153,12 @@ object Crypto { "at the cost of larger key sizes and loss of compatibility." ) - /** - * Corda composite key type - */ + /** Corda composite key type */ + @JvmField val COMPOSITE_KEY = SignatureScheme( 6, "COMPOSITE", - AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), + AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY), emptyList(), CordaSecurityProvider.PROVIDER_NAME, CompositeKey.KEY_ALGORITHM, @@ -163,13 +169,14 @@ object Crypto { ) /** Our default signature scheme if no algorithm is specified (e.g. for key generation). */ + @JvmField val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 /** * Supported digital signature schemes. * Note: Only the schemes added in this map will be supported (see [Crypto]). */ - val supportedSignatureSchemes = listOf( + private val signatureSchemeMap: Map = listOf( RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, @@ -183,15 +190,15 @@ object Crypto { * algorithm identifiers. */ private val algorithmMap: Map - = (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } } - + supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) }) + = (signatureSchemeMap.values.flatMap { scheme -> scheme.alternativeOIDs.map { Pair(it, scheme) } } + + signatureSchemeMap.values.map { Pair(it.signatureOID, it) }) .toMap() // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // that could cause unexpected and suspicious behaviour. // i.e. if someone removes a Provider and then he/she adds a new one with the same name. // The val is private to avoid any harmful state changes. - val providerMap: Map = mapOf( + private val providerMap: Map = mapOf( BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(), CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(), "BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. @@ -201,6 +208,14 @@ object Crypto { addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512)) } + @JvmStatic + fun supportedSignatureSchemes(): List = ArrayList(signatureSchemeMap.values) + + @JvmStatic + fun findProvider(name: String): Provider { + return providerMap[name] ?: throw IllegalArgumentException("Unrecognised provider: $name") + } + init { // This registration is needed for reading back EdDSA key from java keystore. // TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider. @@ -219,8 +234,10 @@ object Crypto { } } + @JvmStatic fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme { - return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}") + return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] + ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}") } /** @@ -231,8 +248,11 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(IllegalArgumentException::class) - fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") + @JvmStatic + fun findSignatureScheme(schemeCodeName: String): SignatureScheme { + return signatureSchemeMap[schemeCodeName] + ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") + } /** * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key]. @@ -242,7 +262,7 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested key type is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun findSignatureScheme(key: PublicKey): SignatureScheme { val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded) return findSignatureScheme(keyInfo.algorithm) @@ -256,7 +276,7 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested key type is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun findSignatureScheme(key: PrivateKey): SignatureScheme { val keyInfo = PrivateKeyInfo.getInstance(key.encoded) return findSignatureScheme(keyInfo.privateKeyAlgorithm) @@ -269,11 +289,12 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { val keyInfo = PrivateKeyInfo.getInstance(encodedKey) val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm) - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } /** @@ -284,8 +305,11 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) - fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey) + @JvmStatic + @Throws(InvalidKeySpecException::class) + fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey { + return decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey) + } /** * Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name. @@ -295,13 +319,18 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) + @JvmStatic + @Throws(InvalidKeySpecException::class) fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } try { - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse) + throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that " + + "it corresponds to the input scheme's code name.", ikse) } } @@ -312,11 +341,12 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for this key factory to produce a private key. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun decodePublicKey(encodedKey: ByteArray): PublicKey { val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey) val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm) - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) } /** @@ -328,8 +358,11 @@ object Crypto { * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) - fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey) + @JvmStatic + @Throws(InvalidKeySpecException::class) + fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey { + return decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey) + } /** * Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name. @@ -340,19 +373,25 @@ object Crypto { * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ - @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) + @JvmStatic + @Throws(InvalidKeySpecException::class) fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } try { - return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) + val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) + return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse) + throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and " + + "that it corresponds to the input scheme's code name.", ikse) } } /** * Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based - * on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray). + * on the [PrivateKey] type, but if the schemeCodeName is known, then better use + * doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray). * @param privateKey the signer's [PrivateKey]. * @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root). * @return the digital signature (in [ByteArray]) on the input message. @@ -360,7 +399,8 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData) /** @@ -373,8 +413,11 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) - fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) + fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray { + return doSign(findSignatureScheme(schemeCodeName), privateKey, clearData) + } /** * Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature]. @@ -386,11 +429,14 @@ object Crypto { * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } + require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" } val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) - if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!") signature.initSign(privateKey) signature.update(clearData) return signature.sign() @@ -398,20 +444,24 @@ object Crypto { /** * Generic way to sign [SignableData] objects with a [PrivateKey]. - * [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as a timestamp or partial and blind signature indicators. - * @param privateKey the signer's [PrivateKey]. + * [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as + * a timestamp or partial and blind signature indicators. + * @param keyPair the signer's [KeyPair]. * @param signableData a [SignableData] object that adds extra information to a transaction. - * @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and the signature metadata. + * @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and + * the signature metadata. * @throws IllegalArgumentException if the signature scheme is not supported for this private key. * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. */ - @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature { val sigKey: SignatureScheme = findSignatureScheme(keyPair.private) val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public) - if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${sigMetaData.schemeCodeName}" + - " is not aligned with the key type: ${sigKey.schemeCodeName}.") + require(sigKey == sigMetaData) { + "Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}." + } val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes) return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata) } @@ -430,8 +480,11 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ + @JvmStatic @Throws(InvalidKeyException::class, SignatureException::class) - fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData) + fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { + return doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData) + } /** * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type. @@ -448,8 +501,11 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ - @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) - fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) + fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { + return doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + } /** * Method to verify a digital signature. @@ -465,9 +521,12 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ - @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!") if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!") val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData) @@ -490,14 +549,16 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty. */ - @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) + @JvmStatic + @Throws(InvalidKeyException::class, SignatureException::class) fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean { val signableData = SignableData(txId, transactionSignature.signatureMetadata) return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes) } /** - * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type. + * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the + * input public key's type. * It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature * do not match it returns false rather than throwing an exception. Normally you should use the function which throws, * as it avoids the risk of failing to test the result. @@ -507,14 +568,20 @@ object Crypto { * the passed-in signatureData is improperly encoded or of the wrong type, * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. */ + @JvmStatic @Throws(SignatureException::class) fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean { val signableData = SignableData(txId, transactionSignature.signatureMetadata) - return isValid(findSignatureScheme(transactionSignature.by), transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes) + return isValid( + findSignatureScheme(transactionSignature.by), + transactionSignature.by, + transactionSignature.bytes, + signableData.serialize().bytes) } /** - * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type. + * Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the + * input public key's type. * It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature * do not match it returns false rather than throwing an exception. Normally you should use the function which throws, * as it avoids the risk of failing to test the result. @@ -527,8 +594,11 @@ object Crypto { * the passed-in signatureData is improperly encoded or of the wrong type, * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. */ + @JvmStatic @Throws(SignatureException::class) - fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { + return isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData) + } /** * Method to verify a digital signature. In comparison to [doVerify] if the key and signature @@ -544,9 +614,12 @@ object Crypto { * if this signatureData scheme is unable to process the input data provided, if the verification is not possible. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(SignatureException::class, IllegalArgumentException::class) + @JvmStatic + @Throws(SignatureException::class) fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) signature.initVerify(publicKey) signature.update(clearData) @@ -560,7 +633,7 @@ object Crypto { * @return a KeyPair for the requested signature scheme code name. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName)) /** @@ -570,10 +643,12 @@ object Crypto { * @return a new [KeyPair] for the requested [SignatureScheme]. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - @Throws(IllegalArgumentException::class) @JvmOverloads + @JvmStatic fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) if (signatureScheme.algSpec != null) keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom()) @@ -638,13 +713,17 @@ object Crypto { * @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. */ + @JvmStatic fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } - when (signatureScheme) { - ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed) - EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed) + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } + return when (signatureScheme) { + ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed) + EDDSA_ED25519_SHA512 -> deriveKeyPairEdDSA(privateKey, seed) + else -> throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is " + + "not currently implemented for ${signatureScheme.schemeCodeName}") } - throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is not currently implemented for ${signatureScheme.schemeCodeName}") } /** @@ -656,6 +735,7 @@ object Crypto { * @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. */ + @JvmStatic fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair { return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed) } @@ -728,11 +808,13 @@ object Crypto { * @return a new [KeyPair] from an entropy input. * @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input. */ + @JvmStatic fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair { - when (signatureScheme) { - EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy) + return when (signatureScheme) { + EDDSA_ED25519_SHA512 -> deriveEdDSAKeyPairFromEntropy(entropy) + else -> throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair " + + "generation: ${signatureScheme.schemeCodeName}") } - throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: ${signatureScheme.schemeCodeName}") } /** @@ -740,6 +822,7 @@ object Crypto { * @param entropy a [BigInteger] value. * @return a new [KeyPair] from an entropy input. */ + @JvmStatic fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) // custom key pair generator from entropy. @@ -766,8 +849,12 @@ object Crypto { } private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter { - override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } - override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } + override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? { + return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) } + } + override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? { + return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) } + } } /** @@ -784,22 +871,29 @@ object Crypto { * @return true if the point lies on the curve or false if it doesn't. * @throws IllegalArgumentException if the requested signature scheme or the key type is not supported. */ - @Throws(IllegalArgumentException::class) + @JvmStatic fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean { - require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } - when (publicKey) { - is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid) - is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve) + require(isSupportedSignatureScheme(signatureScheme)) { + "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" + } + return when (publicKey) { + is BCECPublicKey -> publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid + is EdDSAPublicKey -> publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}") } } // return true if EdDSA publicKey is point at infinity. // For EdDSA a custom function is required as it is not supported by the I2P implementation. - private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) + private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean { + return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) + } /** Check if the requested [SignatureScheme] is supported by the system. */ - fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme + @JvmStatic + fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean { + return signatureScheme.schemeCodeName in signatureSchemeMap + } // validate a key, by checking its algorithmic params. private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { @@ -812,19 +906,19 @@ object Crypto { // check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean { - when (key) { - is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key) - is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + return when (key) { + is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key) + is BCRSAPublicKey, is BCSphincs256PublicKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") } } // check if a private key satisfies algorithm specs. private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { - when (key) { - is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec - is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec - is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + return when (key) { + is BCECPrivateKey -> key.parameters == signatureScheme.algSpec + is EdDSAPrivateKey -> key.params == signatureScheme.algSpec + is BCRSAPrivateKey, is BCSphincs256PrivateKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") } } @@ -838,21 +932,20 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for a supported key factory to produce a private key. */ - fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey { - return Crypto.decodePublicKey(key.encoded) - } + @JvmStatic + fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey = decodePublicKey(key.encoded) /** * Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. - * This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations. + * This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default + * return SUN implementations. * @param key a public key. * @return a supported implementation of the input public key. * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for a supported key factory to produce a private key. */ - fun toSupportedPublicKey(key: PublicKey): PublicKey { - return Crypto.decodePublicKey(key.encoded) - } + @JvmStatic + fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded) /** * Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. @@ -862,7 +955,6 @@ object Crypto { * @throws IllegalArgumentException on not supported scheme or if the given key specification * is inappropriate for a supported key factory to produce a private key. */ - fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { - return Crypto.decodePrivateKey(key.encoded) - } + @JvmStatic + fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt index db7cf6473a..0e0f36c606 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/DigitalSignature.kt @@ -11,9 +11,9 @@ import java.security.SignatureException // should be renamed to match. /** A wrapper around a digital signature. */ @CordaSerializable -open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) { +open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) { /** A digital signature that identifies who the public key is owned by. */ - open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) { + open class WithKey(val by: PublicKey, bytes: ByteArray) : DigitalSignature(bytes) { /** * Utility to simplify the act of verifying a signature. * diff --git a/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt index 0043988ba4..6edd82ca93 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X500NameUtils.kt @@ -1,6 +1,7 @@ @file:JvmName("X500NameUtils") package net.corda.core.crypto +import net.corda.core.internal.toX509CertHolder import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500NameBuilder @@ -57,7 +58,7 @@ val X500Name.locationOrNull: String? get() = try { } catch (e: Exception) { null } -val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject +val X509Certificate.subject: X500Name get() = toX509CertHolder().subject val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) /** diff --git a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt index f9ca0dad7c..a1e31e2f01 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/composite/CompositeKey.kt @@ -42,7 +42,7 @@ class CompositeKey private constructor(val threshold: Int, children: List() { - +class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction, + val myOptionalKeys: Iterable?, + override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic() { + @JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker) companion object { object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") @@ -72,16 +75,14 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, } @Suspendable override fun call(): SignedTransaction { - // TODO: Revisit when key management is properly fleshed out. - // This will break if a party uses anything other than their legalIdentityKey. // Check the signatures which have already been provided and that the transaction is valid. // Usually just the Initiator and possibly an oracle would have signed at this point. - val myKey = serviceHub.myInfo.legalIdentity.owningKey + val myKeys: Iterable = myOptionalKeys ?: listOf(serviceHub.myInfo.legalIdentity.owningKey) val signed = partiallySignedTx.sigs.map { it.by } val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed // One of the signatures collected so far MUST be from the initiator of this flow. - require(partiallySignedTx.sigs.any { it.by == myKey }) { + require(partiallySignedTx.sigs.any { it.by in myKeys }) { "The Initiator of CollectSignaturesFlow must have signed the transaction." } @@ -100,7 +101,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, if (unsigned.isEmpty()) return partiallySignedTx // Collect signatures from all counter-parties and append them to the partially signed transaction. - val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) } + val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) } val stx = partiallySignedTx + counterpartySignatures // Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures. @@ -112,23 +113,32 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, /** * Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache]. + * + * @return a pair of the well known identity to contact for a signature, and the public key that party should sign + * with (this may belong to a confidential identity). */ - @Suspendable private fun keysToParties(keys: Collection): List = keys.map { - // TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key. - val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) + @Suspendable private fun keysToParties(keys: Collection): List> = keys.map { + val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it)) ?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.") - partyNode.legalIdentity + Pair(party, it) } // DOCSTART 1 /** * Get and check the required signature. + * + * @param counterparty the party to request a signature from. + * @param signingKey the key the party should use to sign the transaction. */ - @Suspendable private fun collectSignature(counterparty: Party): TransactionSignature { - // SendTransactionFlow allows otherParty to access our data to resolve the transaction. + @Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature { + // SendTransactionFlow allows counterparty to access our data to resolve the transaction. subFlow(SendTransactionFlow(counterparty, partiallySignedTx)) + // Send the key we expect the counterparty to sign with - this is important where they may have several + // keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward + // for us to check we have the expected signature returned. + send(counterparty, signingKey) return receive(counterparty).unwrap { - require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." } + require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." } it } } @@ -189,9 +199,16 @@ abstract class SignTransactionFlow(val otherParty: Party, progressTracker.currentStep = RECEIVING // Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures. val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false)) + // Receive the signing key that the party requesting the signature expects us to sign with. Having this provided + // means we only have to check we own that one key, rather than matching all keys in the transaction against all + // keys we own. + val signingKey = receive(otherParty).unwrap { + // TODO: We should have a faster way of verifying we own a single key + serviceHub.keyManagementService.filterMyKeys(listOf(it)).single() + } progressTracker.currentStep = VERIFYING // Check that the Responder actually needs to sign. - checkMySignatureRequired(stx) + checkMySignatureRequired(stx, signingKey) // Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's. checkSignatures(stx) stx.tx.toLedgerTransaction(serviceHub).verify() @@ -206,7 +223,7 @@ abstract class SignTransactionFlow(val otherParty: Party, } // Sign and send back our signature to the Initiator. progressTracker.currentStep = SIGNING - val mySignature = serviceHub.createSignature(stx) + val mySignature = serviceHub.createSignature(stx, signingKey) send(otherParty, mySignature) // Return the fully signed transaction once it has been committed. @@ -214,8 +231,10 @@ abstract class SignTransactionFlow(val otherParty: Party, } @Suspendable private fun checkSignatures(stx: SignedTransaction) { - require(stx.sigs.any { it.by == otherParty.owningKey }) { - "The Initiator of CollectSignaturesFlow must have signed the transaction." + val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey) + val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous) + require(otherParty in signingWellKnownIdentities) { + "The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}" } val signed = stx.sigs.map { it.by } val allSigners = stx.tx.requiredSigningKeys @@ -245,10 +264,8 @@ abstract class SignTransactionFlow(val otherParty: Party, */ @Suspendable abstract protected fun checkTransaction(stx: SignedTransaction) - @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) { - // TODO: Revisit when key management is properly fleshed out. - val myKey = serviceHub.myInfo.legalIdentity.owningKey - require(myKey in stx.tx.requiredSigningKeys) { + @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) { + require(signingKey in stx.tx.requiredSigningKeys) { "Party is not a participant for any of the input states of transaction ${stx.id}" } } diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 6a8d7f68f9..d6752b4383 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -140,7 +140,7 @@ abstract class FlowLogic { * network's event horizon time. */ @Suspendable - open fun send(otherParty: Party, payload: Any): Unit = stateMachine.send(otherParty, payload, flowUsedForSessions) + open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions) /** * Invokes the given subflow. This function returns once the subflow completes successfully with the result @@ -239,7 +239,7 @@ abstract class FlowLogic { * Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect * what objects would be serialised at the time of call to a suspending action (e.g. send/receive). * Note: This logic is only available during tests and is not meant to be used during the production deployment. - * Therefore the default implementationdoes nothing. + * Therefore the default implementation does nothing. */ @Suspendable fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java) @@ -256,7 +256,7 @@ abstract class FlowLogic { * Therefore the default implementation does nothing. */ @Suspendable - fun persistFlowStackSnapshot(): Unit = stateMachine.persistFlowStackSnapshot(this::class.java) + fun persistFlowStackSnapshot() = stateMachine.persistFlowStackSnapshot(this::class.java) //////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt b/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt index 5ab6afd37c..0698b40c42 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowStackSnapshot.kt @@ -1,69 +1,21 @@ package net.corda.core.flows -import net.corda.core.utilities.loggerFor -import java.nio.file.Path -import java.util.* - -interface FlowStackSnapshotFactory { - private object Holder { - val INSTANCE: FlowStackSnapshotFactory - - init { - val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull() - INSTANCE = serviceFactory ?: FlowStackSnapshotDefaultFactory() - } - } - - companion object { - val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE } - } - - /** - * Returns flow stack data snapshot extracted from Quasar stack. - * It is designed to be used in the debug mode of the flow execution. - * Note. This logic is only available during tests and is not meant to be used during the production deployment. - * Therefore the default implementation does nothing. - */ - fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? - - /** Stores flow stack snapshot as a json file. The stored shapshot is only partial and consists - * only data (i.e. stack traces and local variables values) relevant to the flow. It does not - * persist corda internal data (e.g. FlowStateMachine). Instead it uses [StackFrameDataToken] to indicate - * the class of the element on the stack. - * The flow stack snapshot is stored in a file located in - * {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/ - * where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform. - * Note. This logic is only available during tests and is not meant to be used during the production deployment. - * Therefore the default implementation does nothing. - */ - fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String): Unit -} - -private class FlowStackSnapshotDefaultFactory : FlowStackSnapshotFactory { - val log = loggerFor() - - override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? { - log.warn("Flow stack snapshot are not supposed to be used in a production deployment") - return null - } - - override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) { - log.warn("Flow stack snapshot are not supposed to be used in a production deployment") - } -} +import java.time.Instant /** * Main data object representing snapshot of the flow stack, extracted from the Quasar stack. */ -data class FlowStackSnapshot constructor( - val timestamp: Long = System.currentTimeMillis(), - val flowClass: String? = null, - val stackFrames: List = listOf() +data class FlowStackSnapshot( + val time: Instant, + val flowClass: String, + val stackFrames: List ) { data class Frame( - val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects] - val stackObjects: List = listOf() - ) + val stackTraceElement: StackTraceElement, // This should be the call that *pushed* the frame of [objects] + val stackObjects: List + ) { + override fun toString(): String = stackTraceElement.toString() + } } /** diff --git a/core/src/main/kotlin/net/corda/core/flows/IdentitySyncFlow.kt b/core/src/main/kotlin/net/corda/core/flows/IdentitySyncFlow.kt new file mode 100644 index 0000000000..a70667c2a6 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/IdentitySyncFlow.kt @@ -0,0 +1,96 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.ContractState +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap + +object IdentitySyncFlow { + /** + * Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential + * identities used in the transaction. This is intended for use as a subflow of another flow, typically between + * transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants + * to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that + * party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that + * identity. + * + * @return a mapping of well known identities to the confidential identities used in the transaction. + */ + // TODO: Can this be triggered automatically from [SendTransactionFlow] + class Send(val otherSides: Set, + val tx: WireTransaction, + override val progressTracker: ProgressTracker) : FlowLogic() { + constructor(otherSide: Party, tx: WireTransaction) : this(setOf(otherSide), tx, tracker()) + + companion object { + object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities") + + fun tracker() = ProgressTracker(SYNCING_IDENTITIES) + } + + @Suspendable + override fun call() { + progressTracker.currentStep = SYNCING_IDENTITIES + val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) + val identities: Set = states.flatMap { it.participants }.toSet() + // Filter participants down to the set of those not in the network map (are not well known) + val confidentialIdentities = identities + .filter { serviceHub.networkMapCache.getNodeByLegalIdentityKey(it.owningKey) == null } + .toList() + val identityCertificates: Map = identities + .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap() + + otherSides.forEach { otherSide -> + val requestedIdentities: List = sendAndReceive>(otherSide, confidentialIdentities).unwrap { req -> + require(req.all { it in identityCertificates.keys }) { "${otherSide} requested a confidential identity not part of transaction: ${tx.id}" } + req + } + val sendIdentities: List = requestedIdentities.map { + val identityCertificate = identityCertificates[it] + if (identityCertificate != null) + identityCertificate + else + throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}") + } + send(otherSide, sendIdentities) + } + } + + } + + /** + * Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which + * we do not yet know about. + */ + class Receive(val otherSide: Party) : FlowLogic() { + companion object { + object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities") + object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities") + } + + override val progressTracker: ProgressTracker = ProgressTracker(RECEIVING_IDENTITIES, RECEIVING_CERTIFICATES) + + @Suspendable + override fun call(): Unit { + progressTracker.currentStep = RECEIVING_IDENTITIES + val allIdentities = receive>(otherSide).unwrap { it } + val unknownIdentities = allIdentities.filter { serviceHub.identityService.partyFromAnonymous(it) == null } + progressTracker.currentStep = RECEIVING_CERTIFICATES + val missingIdentities = sendAndReceive>(otherSide, unknownIdentities) + + // Batch verify the identities we've received, so we know they're all correct before we start storing them in + // the identity service + missingIdentities.unwrap { identities -> + identities.forEach { it.verify(serviceHub.identityService.trustAnchor) } + identities + }.forEach { identity -> + // Store the received confidential identities in the identity service so we have a record of which well known identity they map to. + serviceHub.identityService.verifyAndRegisterIdentity(identity) + } + } + } +} diff --git a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt index 7dc89ae4a5..b549f33e11 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -13,8 +13,7 @@ import java.security.PublicKey @CordaSerializable abstract class AbstractParty(val owningKey: PublicKey) { /** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */ - override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey - + override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey override fun hashCode(): Int = owningKey.hashCode() abstract fun nameOrNull(): X500Name? diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt index b2264a1c6f..ed6e5b3707 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -1,7 +1,6 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference -import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toStringShort import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name @@ -12,11 +11,7 @@ import java.security.PublicKey * information such as name. It is intended to represent a party on the distributed ledger. */ class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) { - // Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party] - // can put in the key and actual name - override fun toString() = "${owningKey.toStringShort()} " - override fun nameOrNull(): X500Name? = null - override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) + override fun toString() = "Anonymous(${owningKey.toStringShort()})" } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index a2f301be75..5652fc2688 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -1,9 +1,11 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference -import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.composite.CompositeKey import net.corda.core.utilities.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import java.security.PublicKey /** @@ -26,10 +28,9 @@ import java.security.PublicKey * @see CompositeKey */ class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { - constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public) - override fun toString() = name.toString() - override fun nameOrNull(): X500Name? = name - + constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo)) + override fun nameOrNull(): X500Name = name fun anonymise(): AnonymousParty = AnonymousParty(owningKey) override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) + override fun toString() = name.toString() } diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt index 557c26d7ee..e6f972798a 100644 --- a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -1,49 +1,42 @@ package net.corda.core.identity -import net.corda.core.serialization.CordaSerializable +import net.corda.core.internal.toX509CertHolder import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder import java.security.PublicKey import java.security.cert.* -import java.util.* /** * A full party plus the X.509 certificate and path linking the party back to a trust root. Equality of * [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party, - * not part of the identifier themselves. While party and certificate can both be derived from the certificate path, - * this class exists in order to ensure the implementation classes of certificates and party public keys are kept stable. + * not part of the identifier themselves. */ -@CordaSerializable -data class PartyAndCertificate(val party: Party, - val certificate: X509CertificateHolder, - val certPath: CertPath) { - constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath) - val name: X500Name - get() = party.name - val owningKey: PublicKey - get() = party.owningKey - - override fun equals(other: Any?): Boolean { - return if (other is PartyAndCertificate) - party == other.party - else - false +//TODO Is VerifiableIdentity a better name? +class PartyAndCertificate(val certPath: CertPath) { + @Transient val certificate: X509CertificateHolder + init { + require(certPath.type == "X.509") { "Only X.509 certificates supported" } + val certs = certPath.certificates + require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" } + certificate = certs[0].toX509CertHolder() } + @Transient val party: Party = Party(certificate) + + val owningKey: PublicKey get() = party.owningKey + val name: X500Name get() = party.name + + operator fun component1(): Party = party + operator fun component2(): X509CertificateHolder = certificate + + override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party override fun hashCode(): Int = party.hashCode() override fun toString(): String = party.toString() - /** - * Verify that the given certificate path is valid and leads to the owning key of the party. - */ + /** Verify the certificate path is valid. */ fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult { - require(certPath.certificates.first() is X509Certificate) { "Subject certificate must be an X.509 certificate" } - require(Arrays.equals(party.owningKey.encoded, certificate.subjectPublicKeyInfo.encoded)) { "Certificate public key must match party owning key" } - require(Arrays.equals(certPath.certificates.first().encoded, certificate.encoded)) { "Certificate path must link to certificate" } - - val validatorParameters = PKIXParameters(setOf(trustAnchor)) + val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false } val validator = CertPathValidator.getInstance("PKIX") - validatorParameters.isRevocationEnabled = false - return validator.validate(certPath, validatorParameters) as PKIXCertPathValidatorResult + return validator.validate(certPath, parameters) as PKIXCertPathValidatorResult } } diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt index 7ee183b348..c855b8ea70 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -3,11 +3,7 @@ package net.corda.core.internal import co.paralleluniverse.fibers.Suspendable import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowContext -import net.corda.core.flows.FlowInitiator -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStackSnapshot -import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction @@ -39,24 +35,11 @@ interface FlowStateMachine { fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map): Unit - /** - * Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect - * what objects would be serialised at the time of call to a suspending action (e.g. send/receive). - */ @Suspendable - fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? + fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? - /** - * Persists a shallow copy of the Quasar stack frames at the time of call to [persistFlowStackSnapshot]. - * Use this to track the monitor evolution of the quasar stack values during the flow execution. - * The flow stack snapshot is stored in a file located in {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/ - * where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform. - * - * Note: With respect to the [flowStackSnapshot], the snapshot being persisted by this method is partial, - * meaning that only flow relevant traces and local variables are persisted. - */ @Suspendable - fun persistFlowStackSnapshot(flowClass: Class<*>): Unit + fun persistFlowStackSnapshot(flowClass: Class>): Unit val serviceHub: ServiceHub val logger: Logger diff --git a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt index e0d9f37580..b362339540 100644 --- a/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt +++ b/core/src/main/kotlin/net/corda/core/internal/InternalUtils.kt @@ -2,6 +2,7 @@ package net.corda.core.internal import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 +import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import rx.Observable import rx.Observer @@ -165,6 +166,9 @@ fun logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T } } +fun java.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded) +fun javax.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded) + /** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */ fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash { val bytes = toByteArray() diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index 77fa327e91..e776b0efce 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -92,7 +92,7 @@ class ResolveTransactionsFlow(private val txHashes: Set, // half way through, it's no big deal, although it might result in us attempting to re-download data // redundantly next time we attempt verification. it.verify(serviceHub) - serviceHub.recordTransactions(it) + serviceHub.recordTransactions(false, it) } return signedTransaction?.let { diff --git a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt index cc60db5386..e9a949d2e9 100644 --- a/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/concurrent/CordaFutureImpl.kt @@ -1,6 +1,6 @@ package net.corda.core.internal.concurrent -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.match import net.corda.core.utilities.getOrThrow diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 601e8c6b11..3133e1c3b0 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -421,6 +421,29 @@ inline fun > CordaRPCOps.startTrac arg3: D ): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) +@Suppress("unused") +inline fun > CordaRPCOps.startTrackedFlow( + @Suppress("unused_parameter") + flowConstructor: (A, B, C, D, E) -> R, + arg0: A, + arg1: B, + arg2: C, + arg3: D, + arg4: E +): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4) + +@Suppress("unused") +inline fun > CordaRPCOps.startTrackedFlow( + @Suppress("unused_parameter") + flowConstructor: (A, B, C, D, E, F) -> R, + arg0: A, + arg1: B, + arg2: C, + arg3: D, + arg4: E, + arg5: F +): FlowProgressHandle = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5) + /** * The Data feed contains a snapshot of the requested data and an [Observable] of future updates. */ diff --git a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt index 44a297e979..6a18a7702b 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt @@ -1,7 +1,10 @@ package net.corda.core.messaging +import net.corda.core.serialization.CordaSerializable + /** The interface for a group of message recipients (which may contain only one recipient) */ +@CordaSerializable interface MessageRecipients /** A base class for the case of point-to-point messages */ diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index 7b71cdcde2..d7f30a4f50 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -24,16 +24,17 @@ data class NodeInfo(val addresses: List, val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services. val legalIdentitiesAndCerts: NonEmptySet, val platformVersion: Int, - var advertisedServices: List = emptyList(), + val advertisedServices: List = emptyList(), val worldMapLocation: WorldMapLocation? = null) { init { - require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" } + require(advertisedServices.none { it.identity == legalIdentityAndCert }) { + "Service identities must be different from node legal identity" + } } - val legalIdentity: Party - get() = legalIdentityAndCert.party - val notaryIdentity: Party - get() = advertisedServices.single { it.info.type.isNotary() }.identity.party + + val legalIdentity: Party get() = legalIdentityAndCert.party + val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party fun serviceIdentities(type: ServiceType): List { - return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party } + return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null } } } diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index 0a885130d9..fb882eb4ad 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -68,18 +68,35 @@ interface ServiceHub : ServicesForResolution { /** * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for - * further processing. This is expected to be run within a database transaction. + * further processing if [notifyVault] is true. This is expected to be run within a database transaction. * * @param txs The transactions to record. + * @param notifyVault indicate if the vault should be notified for the update. */ - fun recordTransactions(txs: Iterable) + fun recordTransactions(notifyVault: Boolean, txs: Iterable) + + /** + * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for + * further processing if [notifyVault] is true. This is expected to be run within a database transaction. + */ + fun recordTransactions(notifyVault: Boolean, first: SignedTransaction, vararg remaining: SignedTransaction) { + recordTransactions(notifyVault, listOf(first, *remaining)) + } /** * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for * further processing. This is expected to be run within a database transaction. */ fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) { - recordTransactions(listOf(first, *remaining)) + recordTransactions(true, first, *remaining) + } + + /** + * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for + * further processing. This is expected to be run within a database transaction. + */ + fun recordTransactions(txs: Iterable) { + recordTransactions(true, txs) } /** @@ -92,8 +109,7 @@ interface ServiceHub : ServicesForResolution { val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) return if (stx.isNotaryChangeTransaction()) { stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index] - } - else stx.tx.outputs[stateRef.index] + } else stx.tx.outputs[stateRef.index] } /** @@ -106,8 +122,7 @@ interface ServiceHub : ServicesForResolution { val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) return if (stx.isNotaryChangeTransaction()) { stx.resolveNotaryChangeTransaction(this).outRef(stateRef.index) - } - else { + } else { stx.tx.outRef(stateRef.index) } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 008d114ebc..916ef6dbe6 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -1,7 +1,10 @@ package net.corda.core.node.services import net.corda.core.contracts.PartyAndReference -import net.corda.core.identity.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder import java.security.InvalidAlgorithmParameterException @@ -114,6 +117,6 @@ interface IdentityService { * @param exactMatch If true, a case sensitive match is done against each component of each X.500 name. */ fun partiesFromName(query: String, exactMatch: Boolean): Set - - class UnknownAnonymousPartyException(msg: String) : Exception(msg) } + +class UnknownAnonymousPartyException(msg: String) : Exception(msg) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 183557f355..ebd70ae93d 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException +import net.corda.core.identity.AbstractParty import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.serialization.CordaSerializable import net.corda.core.toFuture @@ -135,8 +136,7 @@ class Vault(val states: Iterable>) { val recordedTime: Instant, val consumedTime: Instant?, val status: Vault.StateStatus, - val notaryName: String, - val notaryKey: String, + val notary: AbstractParty?, val lockId: String?, val lockUpdateTime: Instant?) } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 0be25265f6..6f15271057 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -4,12 +4,12 @@ package net.corda.core.node.services.vault import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes -import org.bouncycastle.asn1.x500.X500Name import java.time.Instant import java.util.* import javax.persistence.criteria.Predicate @@ -40,6 +40,7 @@ sealed class QueryCriteria { abstract class CommonQueryCriteria : QueryCriteria() { abstract val status: Vault.StateStatus + abstract val contractStateTypes: Set>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) } @@ -49,13 +50,14 @@ sealed class QueryCriteria { * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] */ data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - val contractStateTypes: Set>? = null, + override val contractStateTypes: Set>? = null, val stateRefs: List? = null, - val notaryName: List? = null, + val notary: List? = null, val softLockingCondition: SoftLockingCondition? = null, val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) + super.visit(parser) + return parser.parseCriteria(this) } } @@ -65,9 +67,16 @@ sealed class QueryCriteria { data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List? = null, val uuid: List? = null, val externalId: List? = null, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { + constructor(participants: List? = null, + linearId: List? = null, + status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + contractStateTypes: Set>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes) + override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) + super.visit(parser) + return parser.parseCriteria(this) } } @@ -81,11 +90,13 @@ sealed class QueryCriteria { data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, val owner: List? = null, val quantity: ColumnPredicate? = null, - val issuerPartyName: List? = null, + val issuer: List? = null, val issuerRef: List? = null, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) + super.visit(parser) + return parser.parseCriteria(this) } } @@ -101,9 +112,11 @@ sealed class QueryCriteria { */ data class VaultCustomQueryCriteria @JvmOverloads constructor (val expression: CriteriaExpression, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) + super.visit(parser) + return parser.parseCriteria(this) } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index 88364f3bb2..e3e58ca933 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -9,39 +9,36 @@ import kotlin.reflect.KProperty1 import kotlin.reflect.jvm.javaGetter @CordaSerializable -enum class BinaryLogicalOperator { +interface Operator + +enum class BinaryLogicalOperator : Operator { AND, OR } -@CordaSerializable -enum class EqualityComparisonOperator { +enum class EqualityComparisonOperator : Operator { EQUAL, NOT_EQUAL } -@CordaSerializable -enum class BinaryComparisonOperator { +enum class BinaryComparisonOperator : Operator { LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL, } -@CordaSerializable -enum class NullOperator { +enum class NullOperator : Operator { IS_NULL, NOT_NULL } -@CordaSerializable -enum class LikenessOperator { +enum class LikenessOperator : Operator { LIKE, NOT_LIKE } -@CordaSerializable -enum class CollectionOperator { +enum class CollectionOperator : Operator { IN, NOT_IN } @@ -151,7 +148,7 @@ data class Sort(val columns: Collection) { enum class VaultStateAttribute(val attributeName: String) : Attribute { /** Vault States */ - NOTARY_NAME("notaryName"), + NOTARY_NAME("notary"), CONTRACT_TYPE("contractStateClassName"), STATE_STATUS("stateStatus"), RECORDED_TIME("recordedTime"), diff --git a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt index a74001d912..5a55d9cc92 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/CommonSchema.kt @@ -17,10 +17,17 @@ object CommonSchema /** * First version of the Vault ORM schema */ -object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = listOf(Party::class.java)) { +object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = emptyList()) { @MappedSuperclass open class LinearState( + /** [ContractState] attributes */ + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, + /** * Represents a [LinearState] [UniqueIdentifier] */ @@ -31,18 +38,26 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers var uuid: UUID ) : PersistentState() { - constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id) + constructor(uid: UniqueIdentifier, _participants: Set) + : this(participants = _participants.toMutableSet(), + externalId = uid.externalId, + uuid = uid.id) } @MappedSuperclass open class FungibleState( /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, /** [OwnableState] attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var ownerKey: CommonSchemaV1.Party, + + /** X500Name of owner party **/ + @Column(name = "owner_name") + var owner: AbstractParty, /** [FungibleAsset] attributes * @@ -55,42 +70,12 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers var quantity: Long, /** Issuer attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var issuerParty: CommonSchemaV1.Party, + + /** X500Name of issuer party **/ + @Column(name = "issuer_name") + var issuer: AbstractParty, @Column(name = "issuer_reference") var issuerRef: ByteArray - ) : PersistentState() { - constructor(_participants: Set, _ownerKey: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: ByteArray) - : this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(), - ownerKey = CommonSchemaV1.Party(_ownerKey), - quantity = _quantity, - issuerParty = CommonSchemaV1.Party(_issuerParty), - issuerRef = _issuerRef) - } - - /** - * Party entity (to be replaced by referencing final Identity Schema) - */ - @Entity - @Table(name = "vault_party", - indexes = arrayOf(Index(name = "party_name_idx", columnList = "party_name"))) - class Party( - @Id - @GeneratedValue - @Column(name = "party_id") - var id: Int, - - /** - * [Party] attributes - */ - @Column(name = "party_name") - var name: String, - - @Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?) - var key: String - ) { - constructor(party: AbstractParty) - : this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String()) - } + ) : PersistentState() } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index a2c2d00e8f..a02241aae2 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -1,6 +1,5 @@ package net.corda.core.schemas -import io.requery.Persistable import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.serialization.CordaSerializable @@ -69,4 +68,4 @@ data class PersistentStateRef( /** * Marker interface to denote a persistable Corda state entity that will always have a transaction id and index */ -interface StatePersistable : Persistable \ No newline at end of file +interface StatePersistable \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt index 9ee9503eef..056eb3becf 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/converters/AbstractPartyToX500NameAsStringConverter.kt @@ -2,6 +2,7 @@ package net.corda.core.schemas.converters import net.corda.core.identity.AbstractParty import net.corda.core.node.services.IdentityService +import net.corda.core.utilities.loggerFor import org.bouncycastle.asn1.x500.X500Name import javax.persistence.AttributeConverter import javax.persistence.Converter @@ -17,9 +18,15 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic identitySvc() } + companion object { + val log = loggerFor() + } + override fun convertToDatabaseColumn(party: AbstractParty?): String? { party?.let { - return identityService.partyFromAnonymous(party)?.toString() + val partyName = identityService.partyFromAnonymous(party)?.toString() + if (partyName != null) return partyName + else log.warn ("Identity service unable to resolve AbstractParty: $party") } return null // non resolvable anonymous parties } @@ -27,7 +34,8 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic override fun convertToEntityAttribute(dbData: String?): AbstractParty? { dbData?.let { val party = identityService.partyFromX500Name(X500Name(dbData)) - return party as AbstractParty + if (party != null) return party + else log.warn ("Identity service unable to resolve X500name: $dbData") } return null // non resolvable anonymous parties are stored as nulls } diff --git a/core/src/main/kotlin/net/corda/core/schemas/requery/PersistentState.kt b/core/src/main/kotlin/net/corda/core/schemas/requery/PersistentState.kt deleted file mode 100644 index 787b681807..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/requery/PersistentState.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.corda.core.schemas.requery - -import io.requery.Key -import io.requery.Persistable -import io.requery.Superclass -import net.corda.core.contracts.StateRef -import net.corda.core.schemas.StatePersistable - -import javax.persistence.Column - -object Requery { - /** - * A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The - * [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself). - */ - // TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt - // once we cut-over all existing Hibernate ContractState persistence to Requery - @Superclass interface PersistentState : StatePersistable { - @get:Key - @get:Column(name = "transaction_id", length = 64) - var txId: String - - @get:Key - @get:Column(name = "output_index") - var index: Int - } -} diff --git a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/BlobConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/requery/converters/BlobConverter.kt deleted file mode 100644 index 78e5b9e337..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/BlobConverter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.core.schemas.requery.converters - -import io.requery.Converter -import java.sql.Blob -import javax.sql.rowset.serial.SerialBlob - -/** - * Converts from a [ByteArray] to a [Blob]. - */ -class BlobConverter : Converter { - - override fun getMappedType(): Class = ByteArray::class.java - - override fun getPersistedType(): Class = Blob::class.java - - /** - * creates BLOB(INT.MAX) = 2 GB - */ - override fun getPersistedSize(): Int? = null - - override fun convertToPersisted(value: ByteArray?): Blob? { - return value?.let { SerialBlob(value) } - } - - override fun convertToMapped(type: Class?, value: Blob?): ByteArray? { - return value?.getBytes(1, value.length().toInt()) - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/InstantConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/requery/converters/InstantConverter.kt deleted file mode 100644 index 5c965e8979..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/InstantConverter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.core.schemas.requery.converters - -import io.requery.Converter -import java.sql.Timestamp -import java.time.Instant - -/** - * Converts from a [Instant] to a [java.sql.Timestamp] for Java 8. Note that - * when converting between the time type and the database type all times will be converted to the - * UTC zone offset. - */ -class InstantConverter : Converter { - override fun getMappedType() = Instant::class.java - - override fun getPersistedType() = Timestamp::class.java - - override fun getPersistedSize() = null - - override fun convertToPersisted(value: Instant?) = value?.let { Timestamp.from(it) } - - override fun convertToMapped(type: Class, value: Timestamp?) = value?.toInstant() -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/SecureHashConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/requery/converters/SecureHashConverter.kt deleted file mode 100644 index 1fcea459cc..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/SecureHashConverter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.corda.core.schemas.requery.converters - -import io.requery.Converter -import net.corda.core.crypto.SecureHash - -/** - * Convert from a [SecureHash] to a [String] - */ -class SecureHashConverter : Converter { - - override fun getMappedType(): Class = SecureHash::class.java - - override fun getPersistedType(): Class = String::class.java - - /** - * SecureHash consists of 32 bytes which need VARCHAR(64) in hex - * TODO: think about other hash widths - */ - override fun getPersistedSize(): Int? = 64 - - override fun convertToPersisted(value: SecureHash?): String? { - return value?.toString() - } - - override fun convertToMapped(type: Class, value: String?): SecureHash? { - return value?.let { SecureHash.parse(value) } - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/StateRefConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/requery/converters/StateRefConverter.kt deleted file mode 100644 index 154bfc89f0..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/StateRefConverter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.corda.core.schemas.requery.converters - -import io.requery.Converter -import net.corda.core.contracts.StateRef -import net.corda.core.crypto.SecureHash - -/** - * Converts from a [StateRef] to a Composite Key defined by a [String] txnHash and an [Int] index - */ -class StateRefConverter : Converter> { - override fun getMappedType() = StateRef::class.java - - @Suppress("UNCHECKED_CAST") - override fun getPersistedType() = Pair::class.java as Class> - - override fun getPersistedSize() = null - - override fun convertToPersisted(value: StateRef?) = value?.let { Pair(it.txhash.toString(), it.index) } - - override fun convertToMapped(type: Class, value: Pair?) = value?.let { StateRef(SecureHash.parse(it.first), it.second) } -} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/VaultStateStatusConverter.kt b/core/src/main/kotlin/net/corda/core/schemas/requery/converters/VaultStateStatusConverter.kt deleted file mode 100644 index 89e902aa32..0000000000 --- a/core/src/main/kotlin/net/corda/core/schemas/requery/converters/VaultStateStatusConverter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.corda.core.schemas.requery.converters - -import io.requery.converter.EnumOrdinalConverter -import net.corda.core.node.services.Vault - -/** - * Converter which persists a [Vault.StateStatus] enum using its enum ordinal representation - */ -class VaultStateStatusConverter : EnumOrdinalConverter(Vault.StateStatus::class.java) diff --git a/core/src/main/kotlin/net/corda/core/serialization/DeprecatedConstructorForDeserialization.kt b/core/src/main/kotlin/net/corda/core/serialization/DeprecatedConstructorForDeserialization.kt new file mode 100644 index 0000000000..71d2691e7e --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/DeprecatedConstructorForDeserialization.kt @@ -0,0 +1,14 @@ +package net.corda.core.serialization + +/** + * This annotation is a marker to indicate which secondary constructors should be considered, and in which + * order, for evolving objects during their deserialisation. + * + * Versions will be considered in descending order, currently duplicate versions will result in + * non deterministic behaviour when deserialising objects + */ +@Target(AnnotationTarget.CONSTRUCTOR) +@Retention(AnnotationRetention.RUNTIME) +annotation class DeprecatedConstructorForDeserialization(val version: Int) + + diff --git a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt index 7dab1e0243..b9f2094394 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/SerializationAPI.kt @@ -39,7 +39,7 @@ interface SerializationContext { /** * When serializing, use the format this header sequence represents. */ - val preferedSerializationVersion: ByteSequence + val preferredSerializationVersion: ByteSequence /** * The class loader to use for deserialization. */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 970ca4d728..c3492655f1 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -8,6 +8,7 @@ import net.corda.core.crypto.keys import net.corda.core.identity.Party import net.corda.core.internal.Emoji import net.corda.core.node.ServicesForResolution +import net.corda.core.serialization.CordaSerializable import java.security.PublicKey import java.security.SignatureException import java.util.function.Predicate @@ -17,6 +18,7 @@ import java.util.function.Predicate * by a [SignedTransaction] that carries the signatures over this payload. * The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]). */ +@CordaSerializable data class WireTransaction( /** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */ override val inputs: List, diff --git a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt index a43f86d4ab..fd0b6f87a2 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ByteArrays.kt @@ -139,7 +139,10 @@ fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this) fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this) -private class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() { +/** + * Class is public for serialization purposes + */ +class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() { init { require(offset >= 0 && offset < bytes.size) require(size >= 0 && size <= bytes.size) diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 16f29be8b7..2870b34abf 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -43,31 +43,31 @@ inline fun Logger.debug(msg: () -> String) { * Extension method for easier construction of [Duration]s in terms of integer days: `val twoDays = 2.days`. * @see Duration.ofDays */ -inline val Int.days: Duration get() = Duration.ofDays(toLong()) +val Int.days: Duration get() = Duration.ofDays(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer hours: `val twoHours = 2.hours`. * @see Duration.ofHours */ -inline val Int.hours: Duration get() = Duration.ofHours(toLong()) +val Int.hours: Duration get() = Duration.ofHours(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer minutes: `val twoMinutes = 2.minutes`. * @see Duration.ofMinutes */ -inline val Int.minutes: Duration get() = Duration.ofMinutes(toLong()) +val Int.minutes: Duration get() = Duration.ofMinutes(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer seconds: `val twoSeconds = 2.seconds`. * @see Duration.ofSeconds */ -inline val Int.seconds: Duration get() = Duration.ofSeconds(toLong()) +val Int.seconds: Duration get() = Duration.ofSeconds(toLong()) /** * Extension method for easier construction of [Duration]s in terms of integer milliseconds: `val twoMillis = 2.millis`. * @see Duration.ofMillis */ -inline val Int.millis: Duration get() = Duration.ofMillis(toLong()) +val Int.millis: Duration get() = Duration.ofMillis(toLong()) /** * A simple wrapper that enables the use of Kotlin's `val x by transient { ... }` syntax. Such a property diff --git a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt index 28d0e5c7dc..3472682639 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/NetworkHostAndPort.kt @@ -1,5 +1,6 @@ package net.corda.core.utilities +import net.corda.core.serialization.CordaSerializable import java.net.URI /** @@ -7,6 +8,7 @@ import java.net.URI * @param host a hostname or IP address. IPv6 addresses must not be enclosed in square brackets. * @param port a valid port number. */ +@CordaSerializable data class NetworkHostAndPort(val host: String, val port: Int) { companion object { internal val invalidPortFormat = "Invalid port: %s" diff --git a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt index 4c53791c8f..45923f2238 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt @@ -1,5 +1,7 @@ package net.corda.core.contracts +import net.corda.finance.* +import net.corda.core.contracts.Amount.Companion.sumOrZero import org.junit.Test import java.math.BigDecimal import java.util.* @@ -13,13 +15,6 @@ import kotlin.test.assertTrue * Tests of the [Amount] class. */ class AmountTests { - @Test - fun basicCurrency() { - val expected = 1000L - val amount = Amount(expected, GBP) - assertEquals(expected, amount.quantity) - } - @Test fun `make sure Amount has decimal places`() { val x = Amount(1, Currency.getInstance("USD")) @@ -27,7 +22,7 @@ class AmountTests { } @Test - fun decimalConversion() { + fun `decimal conversion`() { val quantity = 1234L val amountGBP = Amount(quantity, GBP) val expectedGBP = BigDecimal("12.34") @@ -49,22 +44,6 @@ class AmountTests { override fun toString(): String = name } - @Test - fun parsing() { - assertEquals(Amount(1234L, GBP), Amount.parseCurrency("£12.34")) - assertEquals(Amount(1200L, GBP), Amount.parseCurrency("£12")) - assertEquals(Amount(1000L, USD), Amount.parseCurrency("$10")) - assertEquals(Amount(5000L, JPY), Amount.parseCurrency("¥5000")) - assertEquals(Amount(500000L, RUB), Amount.parseCurrency("₽5000")) - assertEquals(Amount(1500000000L, CHF), Amount.parseCurrency("15,000,000 CHF")) - } - - @Test - fun rendering() { - assertEquals("5000 JPY", Amount.parseCurrency("¥5000").toString()) - assertEquals("50.12 USD", Amount.parseCurrency("$50.12").toString()) - } - @Test fun split() { for (baseQuantity in 0..1000) { @@ -81,7 +60,7 @@ class AmountTests { } @Test - fun amountTransfersEquality() { + fun `amount transfers equality`() { val partyA = "A" val partyB = "B" val partyC = "C" @@ -106,7 +85,7 @@ class AmountTests { } @Test - fun amountTransferAggregation() { + fun `amount transfer aggregation`() { val partyA = "A" val partyB = "B" val partyC = "C" @@ -137,7 +116,7 @@ class AmountTests { } @Test - fun amountTransferApply() { + fun `amount transfer apply`() { val partyA = "A" val partyB = "B" val partyC = "C" @@ -182,6 +161,5 @@ class AmountTests { assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)]) assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)]) assertEquals(originalTotals[Pair(partyB, GBP)], newTotals3[Pair(partyB, GBP)]) - } } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt index 0c1540d089..78e93f6b0e 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/LedgerTransactionQueryTests.kt @@ -7,6 +7,7 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.testing.DUMMY_NOTARY import net.corda.testing.TestDependencyInjectionBase import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockServices import org.junit.Before import org.junit.Test @@ -27,6 +28,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() { interface Commands { data class Cmd1(val id: Int) : CommandData, Commands data class Cmd2(val id: Int) : CommandData, Commands + data class Cmd3(val id: Int) : CommandData, Commands // Unused command, required for command not-present checks. } @@ -50,7 +52,11 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() { private fun makeDummyStateAndRef(data: Any): StateAndRef<*> { val dummyState = makeDummyState(data) - val fakeIssueTx = services.signInitialTransaction(TransactionBuilder(notary = DUMMY_NOTARY).addOutputState(dummyState)) + val fakeIssueTx = services.signInitialTransaction( + TransactionBuilder(notary = DUMMY_NOTARY) + .addOutputState(dummyState) + .addCommand(dummyCommand()) + ) services.recordTransactions(fakeIssueTx) val dummyStateRef = StateRef(fakeIssueTx.id, 0) return StateAndRef(TransactionState(dummyState, DUMMY_NOTARY, null), dummyStateRef) @@ -182,7 +188,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() { val intCmd2 = ltx.commandsOfType() assertEquals(5, intCmd2.size) assertEquals(listOf(0, 1, 2, 3, 4), intCmd2.map { it.value.id }) - val notPresentQuery = ltx.commandsOfType(FungibleAsset.Commands.Exit::class.java) + val notPresentQuery = ltx.commandsOfType(Commands.Cmd3::class.java) assertEquals(emptyList(), notPresentQuery) } diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt index 2d05c6ea3d..5da41dd686 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt @@ -1,9 +1,10 @@ package net.corda.core.contracts -import net.corda.contracts.asset.Cash -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash import net.corda.testing.MEGA_CORP import net.corda.testing.MINI_CORP import net.corda.testing.ledger @@ -27,7 +28,6 @@ class TransactionEncumbranceTests { val timeLock = DummyTimeLock.State(FIVE_PM) class DummyTimeLock : Contract { - override val legalContractReference = SecureHash.sha256("DummyTimeLock") override fun verify(tx: LedgerTransaction) { val timeLockInput = tx.inputsOfType().singleOrNull() ?: return val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window") diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt index 2e1603b1b8..bfefdaae15 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionGraphSearchTests.kt @@ -34,14 +34,15 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() { val notaryServices = MockServices(DUMMY_NOTARY_KEY) val originBuilder = TransactionBuilder(DUMMY_NOTARY) - originBuilder.addOutputState(DummyState(random31BitValue())) - originBuilder.addCommand(command, MEGA_CORP_PUBKEY) + .addOutputState(DummyState(random31BitValue())) + .addCommand(command, MEGA_CORP_PUBKEY) val originPtx = megaCorpServices.signInitialTransaction(originBuilder) val originTx = notaryServices.addSignature(originPtx) val inputBuilder = TransactionBuilder(DUMMY_NOTARY) - inputBuilder.addInputState(originTx.tx.outRef(0)) + .addInputState(originTx.tx.outRef(0)) + .addCommand(dummyCommand(MEGA_CORP_PUBKEY)) val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder) val inputTx = megaCorpServices.addSignature(inputPtx) diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt index 2e471ce5db..c6c508b988 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -1,12 +1,12 @@ package net.corda.core.contracts -import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.core.crypto.* import net.corda.core.crypto.composite.CompositeKey import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.testing.* import net.corda.testing.contracts.DummyContract import org.junit.Test diff --git a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt index b34d2cc976..62169f8c21 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CompositeKeyTests.kt @@ -25,16 +25,16 @@ class CompositeKeyTests : TestDependencyInjectionBase() { @JvmField val tempFolder: TemporaryFolder = TemporaryFolder() - val aliceKey = generateKeyPair() - val bobKey = generateKeyPair() - val charlieKey = generateKeyPair() + private val aliceKey = generateKeyPair() + private val bobKey = generateKeyPair() + private val charlieKey = generateKeyPair() - val alicePublicKey: PublicKey = aliceKey.public - val bobPublicKey: PublicKey = bobKey.public - val charliePublicKey: PublicKey = charlieKey.public + private val alicePublicKey: PublicKey = aliceKey.public + private val bobPublicKey: PublicKey = bobKey.public + private val charliePublicKey: PublicKey = charlieKey.public - val message = OpaqueBytes("Transaction".toByteArray()) - val secureHash = message.sha256() + private val message = OpaqueBytes("Transaction".toByteArray()) + private val secureHash = message.sha256() // By lazy is required so that the serialisers are configured before vals initialisation takes place (they internally invoke serialise). val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) } @@ -43,7 +43,6 @@ class CompositeKeyTests : TestDependencyInjectionBase() { @Test fun `(Alice) fulfilled by Alice signature`() { - println(aliceKey.serialize().hash) assertTrue { alicePublicKey.isFulfilledBy(aliceSignature.by) } assertFalse { alicePublicKey.isFulfilledBy(charlieSignature.by) } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index 5468886603..761d77c010 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -343,7 +343,7 @@ class CryptoUtilsTest { // test list of supported algorithms @Test fun `Check supported algorithms`() { - val algList: List = Crypto.supportedSignatureSchemes.keys.toList() + val algList: List = Crypto.supportedSignatureSchemes().map { it.schemeCodeName } val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE") assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index 84fcc0f9e8..985d501bcf 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -1,13 +1,15 @@ package net.corda.core.crypto -import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash.Companion.zeroHash import net.corda.core.identity.Party import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.WireTransaction +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash import net.corda.testing.* import org.junit.Test import java.security.PublicKey diff --git a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt index 767c4e045d..b8b2900d98 100644 --- a/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/AttachmentTests.kt @@ -4,20 +4,18 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 -import net.corda.core.messaging.SingleMessageRecipient -import net.corda.core.node.services.ServiceInfo -import net.corda.core.utilities.getOrThrow import net.corda.core.identity.Party import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchDataFlow +import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.getOrThrow import net.corda.node.services.config.NodeConfiguration -import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.persistence.schemas.requery.AttachmentEntity +import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.node.utilities.DatabaseTransactionManager import net.corda.testing.node.MockNetwork -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties import org.junit.After import org.junit.Before import org.junit.Test @@ -32,13 +30,10 @@ import kotlin.test.assertFailsWith class AttachmentTests { lateinit var mockNet: MockNetwork - lateinit var configuration: RequeryConfiguration @Before fun setUp() { mockNet = MockNetwork() - val dataSourceProperties = makeTestDataSourceProperties() - configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties()) } @After @@ -137,11 +132,9 @@ class AttachmentTests { val corruptBytes = "arggghhhh".toByteArray() System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size) - val corruptAttachment = AttachmentEntity() - corruptAttachment.attId = id - corruptAttachment.content = attachment + val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment) n0.database.transaction { - n0.attachments.session.update(corruptAttachment) + DatabaseTransactionManager.current().session.update(corruptAttachment) } // Get n1 to fetch the attachment. Should receive corrupted bytes. diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 769de29fb4..09a9ae33a3 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -3,13 +3,14 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Command import net.corda.core.contracts.requireThat -import net.corda.testing.contracts.DummyContract +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.getOrThrow import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap import net.corda.testing.MINI_CORP_KEY +import net.corda.testing.contracts.DummyContract import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import org.junit.After @@ -77,16 +78,18 @@ class CollectSignaturesFlowTests { } @InitiatedBy(TestFlow.Initiator::class) - class Responder(val otherParty: Party) : FlowLogic() { + class Responder(val otherParty: Party, val identities: Map) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val state = receive(otherParty).unwrap { it } val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity - val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey }) + val myInputKeys = state.participants.map { it.owningKey } + val myKeys = myInputKeys + (identities[serviceHub.myInfo.legalIdentity] ?: serviceHub.myInfo.legalIdentity).owningKey + val command = Command(DummyContract.Commands.Create(), myInputKeys) val builder = TransactionBuilder(notary).withItems(state, command) val ptx = serviceHub.signInitialTransaction(builder) - val stx = subFlow(CollectSignaturesFlow(ptx)) + val stx = subFlow(CollectSignaturesFlow(ptx, myKeys)) val ftx = subFlow(FinalityFlow(stx)).single() return ftx @@ -103,10 +106,11 @@ class CollectSignaturesFlowTests { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity - val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey }) + val myInputKeys = state.participants.map { it.owningKey } + val command = Command(DummyContract.Commands.Create(), myInputKeys) val builder = TransactionBuilder(notary).withItems(state, command) val ptx = serviceHub.signInitialTransaction(builder) - val stx = subFlow(CollectSignaturesFlow(ptx)) + val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys)) val ftx = subFlow(FinalityFlow(stx)).single() return ftx @@ -136,9 +140,12 @@ class CollectSignaturesFlowTests { @Test fun `successfully collects two signatures`() { + val bConfidentialIdentity = b.services.keyManagementService.freshKeyAndCert(b.info.legalIdentityAndCert, false) + // Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity + a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity) registerFlowOnAllNodes(TestFlowTwo.Responder::class) val magicNumber = 1337 - val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity) + val parties = listOf(a.info.legalIdentity, bConfidentialIdentity.party, c.info.legalIdentity) val state = DummyContract.MultiOwnerState(magicNumber, parties) val flow = a.services.startFlow(TestFlowTwo.Initiator(state)) mockNet.runNetwork() diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index bc05d5be77..951f28b9e3 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -1,22 +1,23 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.Emoji import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.node.services.queryBy -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction -import net.corda.core.internal.Emoji +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.CashIssueFlow +import net.corda.finance.USD +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow import net.corda.node.internal.CordaRPCOpsImpl -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.RPCDriverExposedDSLInterface import net.corda.testing.contracts.DummyContract @@ -42,11 +43,14 @@ class ContractUpgradeFlowTest { @Before fun setup() { mockNet = MockNetwork() - val nodes = mockNet.createSomeNodes() + val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override a = nodes.partyNodes[0] b = nodes.partyNodes[1] notary = nodes.notaryNode.info.notaryIdentity - mockNet.runNetwork() + + val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == nodes.notaryNode.info.notaryIdentity } + a.services.identityService.verifyAndRegisterIdentity(nodeIdentity) + b.services.identityService.verifyAndRegisterIdentity(nodeIdentity) } @After @@ -173,8 +177,7 @@ class ContractUpgradeFlowTest { @Test fun `upgrade Cash to v2`() { // Create some cash. - val anonymous = false - val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture + val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture mockNet.runNetwork() val stx = result.getOrThrow().stx val stateAndRef = stx.tx.outRef(0) @@ -200,7 +203,7 @@ class ContractUpgradeFlowTest { override val contract = CashV2() override val participants = owners - override fun move(newAmount: Amount>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner)) + override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner)) override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)" override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Cash.Commands.Move(), copy(owners = listOf(newOwner))) } @@ -208,9 +211,6 @@ class ContractUpgradeFlowTest { override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner)) override fun verify(tx: LedgerTransaction) {} - - // Dummy Cash contract for testing. - override val legalContractReference = SecureHash.sha256("") } @StartableByRPC diff --git a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt index b280db5dff..d4f96ab44a 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FinalityFlowTests.kt @@ -1,12 +1,12 @@ package net.corda.core.flows -import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.GBP import net.corda.core.contracts.Issued import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow +import net.corda.finance.GBP +import net.corda.finance.contracts.asset.Cash import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import org.junit.After diff --git a/core/src/test/kotlin/net/corda/core/flows/IdentitySyncFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/IdentitySyncFlowTests.kt new file mode 100644 index 0000000000..5bd4cdb349 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/IdentitySyncFlowTests.kt @@ -0,0 +1,87 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.identity.Party +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.testing.ALICE +import net.corda.testing.BOB +import net.corda.testing.DUMMY_NOTARY +import net.corda.testing.node.MockNetwork +import org.junit.After +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class IdentitySyncFlowTests { + lateinit var mockNet: MockNetwork + + @Before + fun before() { + // We run this in parallel threads to help catch any race conditions that may exist. + mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true) + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } + + @Test + fun `sync confidential identities`() { + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) + val alice: Party = aliceNode.services.myInfo.legalIdentity + val bob: Party = bobNode.services.myInfo.legalIdentity + aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert) + aliceNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert) + bobNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert) + bobNode.registerInitiatedFlow(Receive::class.java) + + // Alice issues then pays some cash to a new confidential identity that Bob doesn't know about + val anonymous = true + val ref = OpaqueBytes.of(0x01) + val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notaryNode.services.myInfo.notaryIdentity)) + val issueTx = issueFlow.resultFuture.getOrThrow().stx + val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance().single().owner + assertNull(bobNode.services.identityService.partyFromAnonymous(confidentialIdentity)) + + // Run the flow to sync up the identities + aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow() + val expected = aliceNode.services.identityService.partyFromAnonymous(confidentialIdentity) + val actual = bobNode.services.identityService.partyFromAnonymous(confidentialIdentity) + assertEquals(expected, actual) + } + + /** + * Very lightweight wrapping flow to trigger the counterparty flow that receives the identities. + */ + @InitiatingFlow + class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic() { + @Suspendable + override fun call(): Boolean { + subFlow(IdentitySyncFlow.Send(otherSide, tx)) + // Wait for the counterparty to indicate they're done + return receive(otherSide).unwrap { it } + } + } + + @InitiatedBy(IdentitySyncFlowTests.Initiator::class) + class Receive(val otherSide: Party): FlowLogic() { + @Suspendable + override fun call() { + subFlow(IdentitySyncFlow.Receive(otherSide)) + // Notify the initiator that we've finished syncing + send(otherSide, true) + } + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt index 01e81137f2..2e4cc2429f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ManualFinalityFlowTests.kt @@ -1,12 +1,12 @@ package net.corda.core.flows -import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.GBP import net.corda.core.contracts.Issued import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.getOrThrow +import net.corda.finance.GBP +import net.corda.finance.contracts.asset.Cash import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockServices import org.junit.After diff --git a/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt new file mode 100644 index 0000000000..f6341d9253 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/identity/PartyAndCertificateTest.kt @@ -0,0 +1,26 @@ +package net.corda.core.identity + +import net.corda.core.crypto.entropyToKeyPair +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize +import net.corda.testing.getTestPartyAndCertificate +import net.corda.testing.withTestSerialization +import org.assertj.core.api.Assertions.assertThat +import org.bouncycastle.asn1.x500.X500Name +import org.junit.Test +import java.math.BigInteger + +class PartyAndCertificateTest { + @Test + fun `kryo serialisation`() { + withTestSerialization { + val original = getTestPartyAndCertificate(Party( + X500Name("CN=Test Corp,O=Test Corp,L=Madrid,C=ES"), + entropyToKeyPair(BigInteger.valueOf(83)).public)) + val copy = original.serialize().deserialize() + assertThat(copy).isEqualTo(original).isNotSameAs(original) + assertThat(copy.certPath).isEqualTo(original.certPath) + assertThat(copy.certificate).isEqualTo(original.certificate) + } + } +} diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index faf5bc13d9..b5876f52a9 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -17,8 +17,6 @@ class VaultUpdateTests { override fun verify(tx: LedgerTransaction) { } - - override val legalContractReference: SecureHash = SecureHash.sha256("") } private class DummyState : ContractState { diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 822da1b93e..ed687c252a 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -18,7 +18,7 @@ import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService -import net.corda.node.services.persistence.schemas.requery.AttachmentEntity +import net.corda.node.utilities.DatabaseTransactionManager import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before @@ -53,13 +53,11 @@ private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, conten * @see NodeAttachmentService.importAttachment */ private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) { - with(session) { - withTransaction { - update(AttachmentEntity().apply { - attId = attachmentId - content = data - }) - } + val session = DatabaseTransactionManager.current().session + val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) + attachment?.let { + attachment.content = data + session.save(attachment) } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/CommandsSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/CommandsSerializationTests.kt new file mode 100644 index 0000000000..5c049c5638 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/serialization/CommandsSerializationTests.kt @@ -0,0 +1,18 @@ +package net.corda.core.serialization + +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.contracts.asset.Cash +import net.corda.testing.TestDependencyInjectionBase +import org.junit.Test +import kotlin.test.assertEquals + +class CommandsSerializationTests : TestDependencyInjectionBase() { + + @Test + fun `test cash move serialization`() { + val command = Cash.Commands.Move(CommercialPaper::class.java) + val copiedCommand = command.serialize().deserialize() + + assertEquals(command, copiedCommand) + } +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 492b50826f..20bf1b8f88 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -1,11 +1,11 @@ package net.corda.core.serialization import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds +import net.corda.finance.POUNDS import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Before @@ -19,8 +19,6 @@ val TEST_PROGRAM_ID = TransactionSerializationTests.TestCash() class TransactionSerializationTests : TestDependencyInjectionBase() { class TestCash : Contract { - override val legalContractReference = SecureHash.sha256("TestCash") - override fun verify(tx: LedgerTransaction) { } diff --git a/docs/build.gradle b/docs/build.gradle index 8b2cdf5a1e..b48623ac83 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -18,6 +18,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) { outputDirectory = file("${rootProject.rootDir}/docs/build/html/api/javadoc") processConfigurations = ['compile'] sourceDirs = files('../core/src/main/kotlin', '../client/jfx/src/main/kotlin', '../client/mock/src/main/kotlin', '../client/rpc/src/main/kotlin', '../node/src/main/kotlin', '../finance/src/main/kotlin', '../client/jackson/src/main/kotlin') + includes = ['packages.md'] } task buildDocs(dependsOn: ['apidocs', 'makeDocs']) diff --git a/docs/install-docsite-requirements.sh b/docs/install-docsite-requirements.sh index 7a6ad91182..454b875056 100755 --- a/docs/install-docsite-requirements.sh +++ b/docs/install-docsite-requirements.sh @@ -7,11 +7,15 @@ set -xeo pipefail # Install the virtualenv if [ ! -d "virtualenv" ] then + # If the canonical working directory contains whitespace, virtualenv installs broken scripts. + # But if we pass in an absolute path that uses symlinks to avoid whitespace, that fixes the problem. + # If you run this script manually (not via gradle) from such a path alias, it's available in PWD: + absolutevirtualenv="$PWD/virtualenv" # Check if python2.7 is installed explicitly otherwise fall back to the default python if type "python2.7" > /dev/null; then - virtualenv -p python2.7 virtualenv + virtualenv -p python2.7 "$absolutevirtualenv" else - virtualenv virtualenv + virtualenv "$absolutevirtualenv" fi fi @@ -29,4 +33,4 @@ if [ ! -d "virtualenv/lib/python2.7/site-packages/sphinx" ] then echo "Installing pip dependencies ... " pip install -r requirements.txt -fi \ No newline at end of file +fi diff --git a/docs/packages.md b/docs/packages.md new file mode 100644 index 0000000000..6c620dede6 --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,3 @@ +# Package net.corda.finance.utils + +A collection of utilities for summing financial states, for example, summing obligations to get total debts. \ No newline at end of file diff --git a/docs/source/api-contracts.rst b/docs/source/api-contracts.rst index 504c931e06..6da93efeb3 100644 --- a/docs/source/api-contracts.rst +++ b/docs/source/api-contracts.rst @@ -22,9 +22,7 @@ The ``Contract`` interface is defined as follows: Where: - * ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this - contract type are valid -* ``legalContractReference`` is the hash of the legal prose contract that ``verify`` seeks to express in code +* ``verify(tx: LedgerTransaction)`` determines whether transactions involving states which reference this contract type are valid verify() -------- @@ -187,8 +185,6 @@ execution of ``verify()``: } } } - - override val legalContractReference: SecureHash = SecureHash.sha256("X contract hash") } .. sourcecode:: java @@ -209,9 +205,6 @@ execution of ``verify()``: // Transfer verification logic. } } - - private final SecureHash legalContractReference = SecureHash.sha256("X contract hash"); - @Override public final SecureHash getLegalContractReference() { return legalContractReference; } } Grouping states @@ -297,13 +290,5 @@ We can now verify these groups individually: Legal prose ----------- -Current, ``legalContractReference`` is simply the SHA-256 hash of a contract: - -.. container:: codeset - - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - -In the future, a contract's legal prose will be included as an attachment instead. \ No newline at end of file +Currently, a ``Contract`` subtype may refer to the legal prose it implements via a ``LegalProseReference`` annotation. +In the future, a contract's legal prose will be included as an attachment. diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 9fee067b2c..24db09fcaa 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -90,7 +90,7 @@ in order to initialise the ORM layer. Several examples of entities and mappings are provided in the codebase, including ``Cash.State`` and ``CommercialPaper.State``. For example, here's the first version of the cash schema. -.. literalinclude:: ../../finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt :language: kotlin Identity mapping diff --git a/docs/source/api-states.rst b/docs/source/api-states.rst index de12ffbf7d..6c8f7a0810 100644 --- a/docs/source/api-states.rst +++ b/docs/source/api-states.rst @@ -129,7 +129,7 @@ For example, here is a relatively complex state definition, for a state represen .. container:: codeset - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 92216784fe..1ad50454af 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -11,12 +11,11 @@ API: Transactions Transaction workflow -------------------- -There are four states the transaction can occupy: +At any time, a transaction can occupy one of three states: -* ``TransactionBuilder``, a builder for a transaction in construction -* ``WireTransaction``, an immutable transaction +* ``TransactionBuilder``, a builder for an in-construction transaction * ``SignedTransaction``, an immutable transaction with 1+ associated signatures -* ``LedgerTransaction``, a transaction that can be checked for validity +* ``LedgerTransaction``, an immutable transaction that can be checked for validity Here are the possible transitions between transaction states: @@ -26,8 +25,8 @@ TransactionBuilder ------------------ Creating a builder ^^^^^^^^^^^^^^^^^^ -The first step when creating a transaction is to instantiate a ``TransactionBuilder``. We can create a builder for each -transaction type as follows: +The first step when creating a new transaction is to instantiate a ``TransactionBuilder``. We create a builder for a +transaction as follows: .. container:: codeset @@ -342,7 +341,7 @@ SignedTransaction ----------------- A ``SignedTransaction`` is a combination of: -* An immutable ``WireTransaction`` +* An immutable transaction * A list of signatures over that transaction .. container:: codeset @@ -357,45 +356,9 @@ transaction's signatures. Verifying the transaction's contents ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't -currently have in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow -called ``ReceiveTransactionFlow``. See :doc:`api-flows` for more details. - -When verifying a ``SignedTransaction``, we don't verify the ``SignedTransaction`` *per se*, but rather the -``WireTransaction`` it contains. We extract this ``WireTransaction`` as follows: - -.. container:: codeset - - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt - :language: kotlin - :start-after: DOCSTART 31 - :end-before: DOCEND 31 - :dedent: 12 - - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java - :language: java - :start-after: DOCSTART 31 - :end-before: DOCEND 31 - :dedent: 12 - -However, this still isn't enough. The ``WireTransaction`` holds its inputs as ``StateRef`` instances, and its -attachments as hashes. These do not provide enough information to properly validate the transaction's contents. To -resolve these into actual ``ContractState`` and ``Attachment`` instances, we need to use the ``ServiceHub`` to convert -the ``WireTransaction`` into a ``LedgerTransaction``: - -.. container:: codeset - - .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt - :language: kotlin - :start-after: DOCSTART 32 - :end-before: DOCEND 32 - :dedent: 12 - - .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java - :language: java - :start-after: DOCSTART 32 - :end-before: DOCEND 32 - :dedent: 12 +To verify a transaction, we need to retrieve any states in the transaction chain that our node doesn't currently have +in its local storage from the proposer(s) of the transaction. This process is handled by a built-in flow called +``ReceiveTransactionFlow``. See :doc:`api-flows` for more details. We can now *verify* the transaction to ensure that it satisfies the contracts of all the transaction's input and output states: @@ -414,8 +377,27 @@ states: :end-before: DOCEND 33 :dedent: 12 -We will generally also want to conduct some additional validation of the transaction, beyond what is provided for in -the contract. Here's an example of how we might do this: +We can also conduct additional validation of the transaction, beyond what is performed by its contracts. However, the +``SignedTransaction`` holds its inputs as ``StateRef`` instances, and its attachments as hashes. These do not provide +enough information to properly validate the transaction's contents. To resolve these into actual ``ContractState`` and +``Attachment`` instances, we need to use the ``ServiceHub`` to convert the ``SignedTransaction`` into a +``LedgerTransaction``: + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt + :language: kotlin + :start-after: DOCSTART 32 + :end-before: DOCEND 32 + :dedent: 12 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java + :language: java + :start-after: DOCSTART 32 + :end-before: DOCEND 32 + :dedent: 12 + +We can now perform additional verification. Here's a simple example: .. container:: codeset @@ -558,8 +540,8 @@ Or using another one of our public keys, as follows: Notarising and recording ^^^^^^^^^^^^^^^^^^^^^^^^ -Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See -:doc:`api-flows` for more details. +Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See :doc:`api-flows` for +more details. Notary-change transactions ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index f924e68134..c3b747c1ec 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -70,6 +70,15 @@ There are four implementations of this interface which can be chained together t .. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`) +All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. + +All ``QueryCriteria`` implementations provide an explicitly specifiable set of common attributes: + +1. State status attribute (``Vault.StateStatus``), which defaults to filtering on UNCONSUMED states. + When chaining several criterias using AND / OR, the last value of this attribute will override any previous. +2. Contract state types (``>``), which will contain at minimum one type (by default this will be ``ContractState`` which resolves to all types). + When chaining several criteria using AND / OR, all specified contract state types are combined into a single set. + An example of a custom query is illustrated here: .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -77,10 +86,6 @@ An example of a custom query is illustrated here: :start-after: DOCSTART VaultQueryExample20 :end-before: DOCEND VaultQueryExample20 -All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. - -All ``QueryCriteria`` implementations provide an explicitly specifiable ``StateStatus`` attribute which defaults to filtering on UNCONSUMED states. - .. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. Examples of these ``QueryCriteria`` objects are presented below for Kotlin and Java. @@ -317,22 +322,22 @@ Query for consumed deal states or linear ids, specify a paging specification and Aggregations on cash using various functions: -.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt - :language: kotlin +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java :start-after: DOCSTART VaultJavaQueryExample21 :end-before: DOCEND VaultJavaQueryExample21 Aggregations on cash grouped by currency for various functions: -.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt - :language: kotlin +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java :start-after: DOCSTART VaultJavaQueryExample22 :end-before: DOCEND VaultJavaQueryExample22 Sum aggregation on cash grouped by issuer party and currency and sorted by sum: -.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt - :language: kotlin +.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java + :language: java :start-after: DOCSTART VaultJavaQueryExample23 :end-before: DOCEND VaultJavaQueryExample23 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c0b60571e1..cd88037e0a 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,15 @@ from the previous milestone release. UNRELEASED ---------- +* Vault query common attributes (state status and contract state types) are now handled correctly when using composite + criteria specifications. State status is overridable. Contract states types are aggregatable. + +* Cash selection algorithm is now pluggable (with H2 being the default implementation) + +* Removed usage of Requery ORM library (repalced with JPA/Hibernate) + +* Vault Query performance improvement (replaced expensive per query SQL statement to obtain concrete state types + with single query on start-up followed by dynamic updates using vault state observable)) * Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria`` @@ -40,6 +49,25 @@ UNRELEASED If you specifically need well known identities, use the network map, which is the authoritative source of current well known identities. +* Currency-related API in ``net.corda.core.contracts.ContractsDSL`` has moved to ```net.corda.finance.CurrencyUtils`. + +* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. Use + `CashIssueFlow` instead. + +* Some utility/extension functions (``sumOrThrow``, ``sumOrNull``, ``sumOrZero`` on ``Amount`` and ``Commodity``) + have moved to be static methods on the classes themselves. This improves the API for Java users who no longer + have to see or known about file-level FooKt style classes generated by the Kotlin compile, but means that IntelliJ + no longer auto-suggests these extension functions in completion unless you add import lines for them yourself + (this is Kotlin IDE bug KT-15286). + +* ``:finance`` module now acting as a CorDapp with regard to flow registration, schemas and serializable types. + +* ``WebServerPluginRegistry`` now has a ``customizeJSONSerialization`` which can be overridden to extend the REST JSON + serializers. In particular the IRS demos must now register the ``BusinessCalendar`` serializers. + +* Moved ``:finance`` gradle project files into a ``net.corda.finance`` package namespace. + This may require adjusting imports of Cash flow references and also of ``StartFlow`` permission in ``gradle.build`` files. + Milestone 14 ------------ diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 590e338493..14695b7943 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -60,7 +60,7 @@ Currently, users need special permissions to start flows via RPC. These permissi ] .. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without -logging in. This will be changed in a future release. + logging in. This will be changed in a future release. Observables ----------- diff --git a/docs/source/contract-catalogue.rst b/docs/source/contract-catalogue.rst index 2d0512d405..1424f7575d 100644 --- a/docs/source/contract-catalogue.rst +++ b/docs/source/contract-catalogue.rst @@ -23,6 +23,11 @@ Cash shares a common superclass, ``OnLedgerAsset``, with the Commodity contract. assets which can be issued, moved and exited on chain, with the subclasses handling asset-specific data types and behaviour. +.. note:: Corda supports a pluggable cash selection algorithm by implementing the ``CashSelection`` interface. + The default implementation uses an H2 specific query that can be overridden for different database providers. + Please see ``CashSelectionH2Impl`` and its associated declaration in + ``META-INF\services\net.corda.finance.contracts.asset.CashSelection`` + Commodity --------- diff --git a/docs/source/corda-repo-layout.rst b/docs/source/corda-repo-layout.rst index 0c95bb74f2..6a67deb285 100644 --- a/docs/source/corda-repo-layout.rst +++ b/docs/source/corda-repo-layout.rst @@ -20,7 +20,6 @@ The Corda repository comprises the following folders: * **lib** contains some dependencies * **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence) * **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC -* **node-schemas** contains entity classes used to represent relational database tables * **samples** contains all our Corda demos and code samples * **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the mock network) implementation diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 215bd08b61..9866eda06e 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -64,7 +64,7 @@ applicationDistribution.into("bin") { } task integrationTest(type: Test) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } @@ -89,7 +89,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcUsers = [ ['username' : "user", 'password' : "password", - 'permissions' : ["StartFlow.net.corda.flows.CashFlow"]] + 'permissions' : ["StartFlow.net.corda.finance.flows.CashFlow"]] ] } } diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index fc04ddf82d..288c0015e0 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -1,30 +1,22 @@ package net.corda.docs -import net.corda.contracts.asset.Cash -import net.corda.core.concurrent.CordaFuture -import net.corda.core.contracts.DOLLARS import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow -import net.corda.node.services.startFlowPermission +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.* import net.corda.testing.driver.driver -import net.corda.testing.expect -import net.corda.testing.expectEvents -import net.corda.testing.parallel -import net.corda.testing.sequence import org.junit.Test -import java.util.* -import kotlin.concurrent.thread import kotlin.test.assertEquals class IntegrationTestingTutorial { @@ -33,7 +25,8 @@ class IntegrationTestingTutorial { // START 1 driver { val aliceUser = User("aliceUser", "testPassword1", permissions = setOf( - startFlowPermission() + startFlowPermission(), + startFlowPermission() )) val bobUser = User("bobUser", "testPassword2", permissions = setOf( startFlowPermission() @@ -63,18 +56,21 @@ class IntegrationTestingTutorial { // START 4 val issueRef = OpaqueBytes.of(0) - val futures = Stack>() (1..10).map { i -> - thread { - futures.push(aliceProxy.startFlow(::CashIssueFlow, - i.DOLLARS, - issueRef, - bob.nodeInfo.legalIdentity, - notary.nodeInfo.notaryIdentity - ).returnValue) - } - }.forEach(Thread::join) // Ensure the stack of futures is populated. - futures.forEach { it.getOrThrow() } + aliceProxy.startFlow(::CashIssueFlow, + i.DOLLARS, + issueRef, + notary.nodeInfo.notaryIdentity + ).returnValue + }.transpose().getOrThrow() + // We wait for all of the issuances to run before we start making payments + (1..10).map { i -> + aliceProxy.startFlow(::CashPaymentFlow, + i.DOLLARS, + bob.nodeInfo.legalIdentity, + true + ).returnValue + }.transpose().getOrThrow() bobVaultUpdates.expectEvents { parallel( diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index f4cec78f1c..bf91c9870d 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -3,7 +3,6 @@ package net.corda.docs; import co.paralleluniverse.fibers.Suspendable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; import net.corda.core.crypto.SecureHash; import net.corda.core.crypto.TransactionSignature; @@ -21,6 +20,7 @@ import net.corda.core.transactions.WireTransaction; import net.corda.core.utilities.ProgressTracker; import net.corda.core.utilities.ProgressTracker.Step; import net.corda.core.utilities.UntrustworthyData; +import net.corda.finance.contracts.asset.Cash; import net.corda.testing.contracts.DummyContract; import net.corda.testing.contracts.DummyState; import org.bouncycastle.asn1.x500.X500Name; @@ -28,6 +28,7 @@ import org.jetbrains.annotations.NotNull; import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.security.SignatureException; import java.time.Duration; import java.time.Instant; import java.util.List; @@ -280,7 +281,7 @@ public class FlowCookbookJava { TypeOnlyCommandData typeOnlyCommandData = new DummyContract.Commands.Create(); // 2. Include additional data which can be used by the contract // during verification, alongside fulfilling the roles above - CommandData commandDataWithData = new Cash.Commands.Issue(12345678); + CommandData commandDataWithData = new Cash.Commands.Issue(); // Attachments are identified by their hash. // The attachment with the corresponding hash must have been @@ -394,15 +395,18 @@ public class FlowCookbookJava { ----------------------------*/ progressTracker.setCurrentStep(TX_VERIFICATION); - // Verifying a transaction will also verify every transaction in the transaction's dependency chain, which will require - // transaction data access on counterparty's node. The ``SendTransactionFlow`` can be used to automate the sending - // and data vending process. The ``SendTransactionFlow`` will listen for data request until the transaction - // is resolved and verified on the other side: + // Verifying a transaction will also verify every transaction in + // the transaction's dependency chain, which will require + // transaction data access on counterparty's node. The + // ``SendTransactionFlow`` can be used to automate the sending and + // data vending process. The ``SendTransactionFlow`` will listen + // for data request until the transaction is resolved and verified + // on the other side: // DOCSTART 12 subFlow(new SendTransactionFlow(counterparty, twiceSignedTx)); // Optional request verification to further restrict data access. - subFlow(new SendTransactionFlow(counterparty, twiceSignedTx){ + subFlow(new SendTransactionFlow(counterparty, twiceSignedTx) { @Override protected void verifyDataRequest(@NotNull FetchDataFlow.Request.Data dataRequest) { // Extra request verification. @@ -425,41 +429,43 @@ public class FlowCookbookJava { List> resolvedStateAndRef = subFlow(new ReceiveStateAndRefFlow(counterparty)); // DOCEND 14 - // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` - // with signatures over this ``WireTransaction``. We don't verify - // a signed transaction per se, but rather the ``WireTransaction`` - // it contains. - // DOCSTART 31 - WireTransaction wireTx = twiceSignedTx.getTx(); - // DOCEND 31 - // Before we can verify the transaction, we need the - // ``ServiceHub`` to use our node's local storage to resolve the - // transaction's inputs and attachments into actual objects, - // rather than just references. We do this by converting the - // ``WireTransaction`` into a ``LedgerTransaction``. - // DOCSTART 32 - LedgerTransaction ledgerTx = wireTx.toLedgerTransaction(getServiceHub()); - // DOCEND 32 - // We can now verify the transaction. - // DOCSTART 33 - ledgerTx.verify(); - // DOCEND 33 + try { - // We'll often want to perform our own additional verification - // too. Just because a transaction is valid based on the contract - // rules and requires our signature doesn't mean we have to - // sign it! We need to make sure the transaction represents an - // agreement we actually want to enter into. - // DOCSTART 34 - DummyState outputState = (DummyState) wireTx.getOutputs().get(0).getData(); - if (outputState.getMagicNumber() != 777) { - // ``FlowException`` is a special exception type. It will be - // propagated back to any counterparty flows waiting for a - // message from this flow, notifying them that the flow has - // failed. - throw new FlowException("We expected a magic number of 777."); + // We can now verify the transaction to ensure that it satisfies + // the contracts of all the transaction's input and output states. + // DOCSTART 33 + twiceSignedTx.verify(getServiceHub()); + // DOCEND 33 + + // We'll often want to perform our own additional verification + // too. Just because a transaction is valid based on the contract + // rules and requires our signature doesn't mean we have to + // sign it! We need to make sure the transaction represents an + // agreement we actually want to enter into. + + // To do this, we need to convert our ``SignedTransaction`` + // into a ``LedgerTransaction``. This will use our ServiceHub + // to resolve the transaction's inputs and attachments into + // actual objects, rather than just references. + // DOCSTART 32 + LedgerTransaction ledgerTx = twiceSignedTx.toLedgerTransaction(getServiceHub()); + // DOCEND 32 + + // We can now perform our additional verification. + // DOCSTART 34 + DummyState outputState = ledgerTx.outputsOfType(DummyState.class).get(0); + if (outputState.getMagicNumber() != 777) { + // ``FlowException`` is a special exception type. It will be + // propagated back to any counterparty flows waiting for a + // message from this flow, notifying them that the flow has + // failed. + throw new FlowException("We expected a magic number of 777."); + } + // DOCEND 34 + + } catch (GeneralSecurityException e) { + // Handle this as required. } - // DOCEND 34 // Of course, if you are not a required signer on the transaction, // you have no power to decide whether it is valid or not. If it diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index fe2255b3e7..6f6c080322 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -1,9 +1,6 @@ package net.corda.docs -import net.corda.client.rpc.notUsed -import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount -import net.corda.core.contracts.USD import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultQueryBy @@ -13,10 +10,12 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationCustomization import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes -import net.corda.flows.CashExitFlow -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow -import net.corda.node.services.startFlowPermission +import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashExitFlow +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.ALICE @@ -128,7 +127,7 @@ fun generateTransactions(proxy: CordaRPCOps) { proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me) } else { val quantity = Math.abs(random.nextLong() % 1000) - proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, me, notary) + proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, notary) ownedQuantity += quantity } } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt index 142c0e2920..fff94dbbbd 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/CustomVaultQuery.kt @@ -16,7 +16,10 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.loggerFor import net.corda.core.utilities.unwrap -import net.corda.flows.* +import net.corda.finance.flows.AbstractCashFlow +import net.corda.finance.flows.CashException +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow import java.util.* // DOCSTART CustomVaultQuery @@ -132,8 +135,7 @@ object TopupIssuerFlow { val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity // invoke Cash subflow to issue Asset progressTracker.currentStep = ISSUING - val issueRecipient = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous = false) + val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, notaryParty) val issueTx = subFlow(issueCashFlow) // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) // short-circuit when issuing to self diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index b5522a22a1..904d4fa95d 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -3,7 +3,6 @@ package net.corda.docs import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature @@ -17,12 +16,12 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Step import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.seconds import net.corda.core.utilities.unwrap +import net.corda.finance.contracts.asset.Cash import net.corda.testing.ALICE_PUBKEY import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState @@ -262,8 +261,8 @@ object FlowCookbook { // fork the contract's verification logic. val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create() // 2. Include additional data which can be used by the contract - // during verification, alongside fulfilling the roles above - val commandDataWithData: CommandData = Cash.Commands.Issue(nonce = 12345678) + // during verification, alongside fulfilling the roles above. + val commandDataWithData: CommandData = Cash.Commands.Issue() // Attachments are identified by their hash. // The attachment with the corresponding hash must have been @@ -379,10 +378,13 @@ object FlowCookbook { ---------------------------**/ progressTracker.currentStep = TX_VERIFICATION - // Verifying a transaction will also verify every transaction in the transaction's dependency chain, which will require - // transaction data access on counterparty's node. The ``SendTransactionFlow`` can be used to automate the sending - // and data vending process. The ``SendTransactionFlow`` will listen for data request until the transaction - // is resolved and verified on the other side: + // Verifying a transaction will also verify every transaction in + // the transaction's dependency chain, which will require + // transaction data access on counterparty's node. The + // ``SendTransactionFlow`` can be used to automate the sending and + // data vending process. The ``SendTransactionFlow`` will listen + // for data request until the transaction is resolved and verified + // on the other side: // DOCSTART 12 subFlow(SendTransactionFlow(counterparty, twiceSignedTx)) @@ -401,7 +403,8 @@ object FlowCookbook { val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterparty)) // DOCEND 13 - // We can also send and receive a `StateAndRef` dependency chain and automatically resolve its dependencies. + // We can also send and receive a `StateAndRef` dependency chain + // and automatically resolve its dependencies. // DOCSTART 14 subFlow(SendStateAndRefFlow(counterparty, dummyStates)) @@ -409,24 +412,10 @@ object FlowCookbook { val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow(counterparty)) // DOCEND 14 - // A ``SignedTransaction`` is a pairing of a ``WireTransaction`` - // with signatures over this ``WireTransaction``. We don't verify - // a signed transaction per se, but rather the ``WireTransaction`` - // it contains. - // DOCSTART 31 - val wireTx: WireTransaction = twiceSignedTx.tx - // DOCEND 31 - // Before we can verify the transaction, we need the - // ``ServiceHub`` to use our node's local storage to resolve the - // transaction's inputs and attachments into actual objects, - // rather than just references. We do this by converting the - // ``WireTransaction`` into a ``LedgerTransaction``. - // DOCSTART 32 - val ledgerTx: LedgerTransaction = wireTx.toLedgerTransaction(serviceHub) - // DOCEND 32 - // We can now verify the transaction. + // We can now verify the transaction to ensure that it satisfies + // the contracts of all the transaction's input and output states. // DOCSTART 33 - ledgerTx.verify() + twiceSignedTx.verify(serviceHub) // DOCEND 33 // We'll often want to perform our own additional verification @@ -434,8 +423,18 @@ object FlowCookbook { // rules and requires our signature doesn't mean we have to // sign it! We need to make sure the transaction represents an // agreement we actually want to enter into. + + // To do this, we need to convert our ``SignedTransaction`` + // into a ``LedgerTransaction``. This will use our ServiceHub + // to resolve the transaction's inputs and attachments into + // actual objects, rather than just references. + // DOCSTART 32 + val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub) + // DOCEND 32 + + // We can now perform our additional verification. // DOCSTART 34 - val outputState: DummyState = wireTx.outputsOfType().single() + val outputState: DummyState = ledgerTx.outputsOfType().single() if (outputState.magicNumber == 777) { // ``FlowException`` is a special exception type. It will be // propagated back to any counterparty flows waiting for a diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index 14d0efb9fa..759cabb4d9 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -1,17 +1,14 @@ package net.corda.docs import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.Issued import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.withoutIssuer import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.* +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.vault.QueryCriteria @@ -20,7 +17,8 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.unwrap -import net.corda.schemas.CashSchemaV1 +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.schemas.CashSchemaV1 import java.util.* @CordaSerializable @@ -42,8 +40,8 @@ private fun gatherOurInputs(serviceHub: ServiceHub, val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") } val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties) - val notaryName = if (notary != null) notary.name else serviceHub.networkMapCache.getAnyNotary()!!.name - val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = listOf(notaryName)) + val notaries = notary ?: serviceHub.networkMapCache.getAnyNotary() + val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = listOf(notaries as AbstractParty)) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) } val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt index efe6d398df..daa6d6e149 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt @@ -2,7 +2,6 @@ package net.corda.docs import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.containsAny import net.corda.core.flows.FinalityFlow @@ -11,8 +10,6 @@ import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub -import net.corda.core.node.services.Vault import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.serialization.CordaSerializable @@ -35,7 +32,7 @@ enum class WorkflowState { * Minimal contract to encode a simple workflow with one initial state and two possible eventual states. * It is assumed one party unilaterally submits and the other manually retrieves the deal and completes it. */ -data class TradeApprovalContract(override val legalContractReference: SecureHash = SecureHash.sha256("Example of workflow type transaction")) : Contract { +data class TradeApprovalContract(private val blank: Void? = null) : Contract { interface Commands : CommandData { class Issue : TypeOnlyCommandData(), Commands // Record receipt of deal details diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt index 1995668fb0..0a2aa861be 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/CustomVaultQueryTest.kt @@ -1,11 +1,12 @@ package net.corda.docs -import net.corda.contracts.getCashBalances -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.CashIssueFlow +import net.corda.finance.* +import net.corda.finance.contracts.getCashBalances +import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.testing.DUMMY_NOTARY @@ -65,9 +66,7 @@ class CustomVaultQueryTest { // Use NodeA as issuer and create some dollars val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(amountToIssue, OpaqueBytes.of(0x01), - nodeA.info.legalIdentity, - notaryNode.info.notaryIdentity, - false)) + notaryNode.info.notaryIdentity)) // Wait for the flow to stop and print flowHandle1.resultFuture.getOrThrow() } diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index d937aaaa65..b85fba910b 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -1,12 +1,12 @@ package net.corda.docs -import net.corda.contracts.getCashBalances -import net.corda.core.contracts.* import net.corda.core.node.services.ServiceInfo import net.corda.core.toFuture import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.CashIssueFlow +import net.corda.finance.* +import net.corda.finance.contracts.getCashBalances +import net.corda.finance.flows.CashIssueFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.testing.DUMMY_NOTARY @@ -46,9 +46,7 @@ class FxTransactionBuildTutorialTest { // Use NodeA as issuer and create some dollars val flowHandle1 = nodeA.services.startFlow(CashIssueFlow(DOLLARS(1000), OpaqueBytes.of(0x01), - nodeA.info.legalIdentity, - notaryNode.info.notaryIdentity, - false)) + notaryNode.info.notaryIdentity)) // Wait for the flow to stop and print flowHandle1.resultFuture.getOrThrow() printBalances() @@ -56,9 +54,7 @@ class FxTransactionBuildTutorialTest { // Using NodeB as Issuer create some pounds. val flowHandle2 = nodeB.services.startFlow(CashIssueFlow(POUNDS(1000), OpaqueBytes.of(0x01), - nodeB.info.legalIdentity, - notaryNode.info.notaryIdentity, - false)) + notaryNode.info.notaryIdentity)) // Wait for flow to come to an end and print flowHandle2.resultFuture.getOrThrow() printBalances() diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index d25ae31313..4aa5c5369d 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -94,7 +94,7 @@ Our flow has two parties (B and S for buyer and seller) and will proceed as foll recording the transaction in B's local vault, and then sending it on to S who also checks it and commits the transaction to S's local vault. -You can find the implementation of this flow in the file ``finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt``. +You can find the implementation of this flow in the file ``finance/src/main/kotlin/net/corda/finance/TwoPartyTradeFlow.kt``. Assuming no malicious termination, they both end the flow being in possession of a valid, signed transaction that represents an atomic asset swap. @@ -235,7 +235,7 @@ Let's implement the ``Seller.call`` method. This will be run when the flow is in .. container:: codeset - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt :language: kotlin :start-after: DOCSTART 4 :end-before: DOCEND 4 @@ -269,7 +269,7 @@ OK, let's do the same for the buyer side: .. container:: codeset - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt :language: kotlin :start-after: DOCSTART 1 :end-before: DOCEND 1 @@ -428,7 +428,7 @@ override ``checkTransaction()`` to add our own custom validation logic: .. container:: codeset - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt :language: kotlin :start-after: DOCSTART 5 :end-before: DOCEND 5 @@ -504,7 +504,7 @@ A flow might declare some steps with code inside the flow class like this: .. container:: codeset - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 @@ -545,7 +545,7 @@ steps by overriding the ``Step`` class like this: .. container:: codeset - .. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt :language: kotlin :start-after: DOCSTART 3 :end-before: DOCEND 3 diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst index 31bebc2495..c54e5cf6da 100644 --- a/docs/source/hello-world-contract.rst +++ b/docs/source/hello-world-contract.rst @@ -37,17 +37,11 @@ Just as every Corda state must implement the ``ContractState`` interface, every // Implements the contract constraints in code. @Throws(IllegalArgumentException::class) fun verify(tx: LedgerTransaction) - - // Expresses the contract constraints as legal prose. - val legalContractReference: SecureHash } You can read about function declarations in Kotlin `here `_. -We can see that ``Contract`` expresses its constraints in two ways: - -* In legal prose, through a hash referencing a legal contract that expresses the contract's constraints in legal prose -* In code, through a ``verify`` function that takes a transaction as input, and: +We can see that ``Contract`` expresses its constraints through a ``verify`` function that takes a transaction as input, and: * Throws an ``IllegalArgumentException`` if it rejects the transaction proposal * Returns silently if it accepts the transaction proposal @@ -67,7 +61,7 @@ transfer them or redeem them for cash. One way to enforce this behaviour would b * Its value must be non-negative * The lender and the borrower cannot be the same entity - * The IOU's borrower must sign the transaction + * The IOU's lender must sign the transaction We can picture this transaction as follows: @@ -79,18 +73,19 @@ Defining IOUContract -------------------- Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or -``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: +``App.kt`` and updating ``TemplateContract`` to define an ``IOUContract``: .. container:: codeset .. code-block:: kotlin - package com.iou + ... import net.corda.core.contracts.* - import net.corda.core.crypto.SecureHash - open class IOUContract : Contract { + ... + + class IOUContract : Contract { // Our Create command. class Create : CommandData @@ -109,19 +104,16 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi // Constraints on the signers. "There must only be one signer." using (command.signers.toSet().size == 1) - "The signer must be the borrower." using (command.signers.contains(out.borrower.owningKey)) + "The signer must be the lender." using (command.signers.contains(out.lender.owningKey)) } } - - // The legal contract reference - we'll leave this as a dummy hash for now. - override val legalContractReference = SecureHash.zeroHash } .. code-block:: java - package com.iou; + package com.template.contract; - import com.google.common.collect.ImmutableSet; + import com.template.state.IOUState; import net.corda.core.contracts.AuthenticatedObject; import net.corda.core.contracts.CommandData; import net.corda.core.contracts.Contract; @@ -146,25 +138,23 @@ Let's write a contract that enforces these constraints. We'll do this by modifyi check.using("There should be one output state of type IOUState.", tx.getOutputs().size() == 1); // IOU-specific constraints. - final IOUState out = (IOUState) tx.getOutputs().getData().get(0); + final IOUState out = (IOUState) tx.getOutputs().get(0).getData(); final Party lender = out.getLender(); final Party borrower = out.getBorrower(); check.using("The IOU's value must be non-negative.",out.getValue() > 0); check.using("The lender and the borrower cannot be the same entity.", lender != borrower); // Constraints on the signers. - check.using("There must only be one signer.", ImmutableSet.of(command.getSigners()).size() == 1); - check.using("The signer must be the borrower.", command.getSigners().contains(borrower.getOwningKey())); + check.using("There must only be one signer.", command.getSigners().size() == 1); + check.using("The signer must be the lender.", command.getSigners().contains(lender.getOwningKey())); return null; }); } - - // The legal contract reference - we'll leave this as a dummy hash for now. - private final SecureHash legalContractReference = SecureHash.Companion.getZeroHash(); - @Override public final SecureHash getLegalContractReference() { return legalContractReference; } } +If you're following along in Java, you'll also need to rename ``TemplateContract.java`` to ``IOUContract.java``. + Let's walk through this code step by step. The Create command @@ -175,7 +165,7 @@ The first thing we add to our contract is a *command*. Commands serve two functi example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one redeeming an IOU * They allow us to define the required signers for the transaction. For example, IOU creation might require signatures - from the borrower alone, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender + from the lender only, whereas the transfer of an IOU might require signatures from both the IOU's borrower and lender Our contract has one command, a ``Create`` command. All commands must implement the ``CommandData`` interface. @@ -215,7 +205,7 @@ following are true: * The transaction has inputs * The transaction doesn't have exactly one output * The IOU itself is invalid -* The transaction doesn't require the borrower's signature +* The transaction doesn't require the lender's signature Command constraints ~~~~~~~~~~~~~~~~~~~ @@ -254,7 +244,7 @@ other statements - in this case, we're extracting the transaction's single ``IOU Signer constraints ~~~~~~~~~~~~~~~~~~ -Finally, we require the borrower's signature on the transaction. A transaction's required signers is equal to the union +Finally, we require the lender's signature on the transaction. A transaction's required signers is equal to the union of all the signers listed on the commands. We therefore extract the signers from the ``Create`` command we retrieved earlier. @@ -266,7 +256,7 @@ We've now written an ``IOUContract`` constraining the evolution of each ``IOUSta * Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a ``Create`` command * The ``IOUState`` created by the issuance transaction must have a non-negative value, and the lender and borrower - must be different entities. + must be different entities Before we move on, make sure you go back and modify ``IOUState`` to point to the new ``IOUContract`` class. diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst index c86c81191e..1adb34a1e5 100644 --- a/docs/source/hello-world-flow.rst +++ b/docs/source/hello-world-flow.rst @@ -33,24 +33,20 @@ FlowLogic Flows are implemented as ``FlowLogic`` subclasses. You define the steps taken by the flow by overriding ``FlowLogic.call``. -We'll write our flow in either ``TemplateFlow.java`` or ``TemplateFlow.kt``. Overwrite the existing template code with -the following: +We'll write our flow in either ``TemplateFlow.java`` or ``App.kt``. Overwrite both the existing flows in the template +with the following: .. container:: codeset .. code-block:: kotlin - package com.iou + ... - import co.paralleluniverse.fibers.Suspendable - import net.corda.core.contracts.Command - import net.corda.core.flows.FlowLogic - import net.corda.core.flows.InitiatingFlow - import net.corda.core.flows.StartableByRPC - import net.corda.core.identity.Party - import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker - import net.corda.core.flows.FinalityFlow + import net.corda.core.transactions.TransactionBuilder + import net.corda.core.flows.* + + ... @InitiatingFlow @StartableByRPC @@ -79,7 +75,7 @@ the following: txBuilder.verify(serviceHub) // Signing the transaction. - val signedTx = serviceHub.toSignedTransaction(txBuilder) + val signedTx = serviceHub.signInitialTransaction(txBuilder) // Finalising the transaction. subFlow(FinalityFlow(signedTx)) @@ -88,19 +84,17 @@ the following: .. code-block:: java - package com.iou; + package com.template.flow; import co.paralleluniverse.fibers.Suspendable; + import com.template.contract.IOUContract; + import com.template.state.IOUState; import net.corda.core.contracts.Command; - import net.corda.core.flows.FlowException; - import net.corda.core.flows.FlowLogic; - import net.corda.core.flows.InitiatingFlow; - import net.corda.core.flows.StartableByRPC; + import net.corda.core.flows.*; import net.corda.core.identity.Party; import net.corda.core.transactions.SignedTransaction; import net.corda.core.transactions.TransactionBuilder; import net.corda.core.utilities.ProgressTracker; - import net.corda.flows.FinalityFlow; @InitiatingFlow @StartableByRPC @@ -118,6 +112,11 @@ the following: this.otherParty = otherParty; } + @Override + public ProgressTracker getProgressTracker() { + return progressTracker; + } + /** * The flow logic is encapsulated within the call() method. */ @@ -128,7 +127,7 @@ the following: final Party me = getServiceHub().getMyInfo().getLegalIdentity(); final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); - // We create a transaction builder + // We create a transaction builder. final TransactionBuilder txBuilder = new TransactionBuilder(); txBuilder.setNotary(notary); @@ -141,7 +140,7 @@ the following: txBuilder.verify(getServiceHub()); // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().toSignedTransaction(txBuilder); + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Finalising the transaction. subFlow(new FinalityFlow(signedTx)); @@ -150,6 +149,8 @@ the following: } } +If you're following along in Java, you'll also need to rename ``TemplateFlow.java`` to ``IOUFlow.java``. + We now have our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. There's a few things to note: * ``FlowLogic.call`` has a return type that matches the type parameter passed to ``FlowLogic`` - this is type returned @@ -163,6 +164,9 @@ We now have our own ``FlowLogic`` subclass that overrides ``FlowLogic.call``. Th * ``@InitiatingFlow`` means that this flow can be started directly by the node * ``StartableByRPC`` allows the node owner to start this flow via an RPC call +* We override the progress tracker, even though we are not providing any progress tracker steps yet. The progress + tracker is required for the node shell to establish when the flow has ended + Let's walk through the steps of ``FlowLogic.call`` one-by-one: Retrieving participant information diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst index b71eefd867..78788090c3 100644 --- a/docs/source/hello-world-running.rst +++ b/docs/source/hello-world-running.rst @@ -9,29 +9,32 @@ Running our CorDapp Now that we've written a CorDapp, it's time to test it by running it on some real Corda nodes. +Clean up +-------- +Before running our node, delete the ``client/TemplateClient.java`` (for Java) or ``client/TemplateClient.kt`` (for +Kotlin) file. We won't be using it, and it will cause build errors unless we remove it. + Deploying our CorDapp --------------------- -Let's take a look at the nodes we're going to deploy. Open the project's build file under ``java-source/build.gradle`` -or ``kotlin-source/build.gradle`` and scroll down to the ``task deployNodes`` section. This section defines four -nodes - the Controller, and NodeA, NodeB and NodeC: +Let's take a look at the nodes we're going to deploy. Open the project's ``build.gradle`` file and scroll down to the +``task deployNodes`` section. This section defines three nodes - the Controller, NodeA, and NodeB: .. container:: codeset .. code-block:: kotlin - task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" + networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK" node { - name "CN=Controller,O=R3,OU=corda,L=London,C=GB" + name "CN=Controller,O=R3,OU=corda,L=London,C=UK" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 - webPort 10004 cordapps = [] } node { - name "CN=NodeA,O=NodeA,L=London,C=GB" + name "CN=NodeA,O=NodeA,L=London,C=UK" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -48,15 +51,6 @@ nodes - the Controller, and NodeA, NodeB and NodeC: cordapps = [] rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] } - node { - name "CN=NodeC,O=NodeC,L=Paris,C=FR" - advertisedServices = [] - p2pPort 10011 - rpcPort 10012 - webPort 10013 - cordapps = [] - rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] - } } We have three standard nodes, plus a special Controller node that is running the network map service, and is also @@ -80,8 +74,8 @@ We can do that now by running the following commands from the root of the projec Running the nodes ----------------- -Running ``deployNodes`` will build the nodes under both ``java-source/build/nodes`` and ``kotlin-source/build/nodes``. -If we navigate to one of these folders, we'll see four node folder. Each node folder has the following structure: +Running ``deployNodes`` will build the nodes under ``build/nodes``. If we navigate to one of these folders, we'll see +the three node folders. Each node folder has the following structure: .. code:: python @@ -97,17 +91,11 @@ Let's start the nodes by running the following commands from the root of the pro .. code:: python - // On Windows for a Java CorDapp - java-source/build/nodes/runnodes.bat + // On Windows + build/nodes/runnodes.bat - // On Windows for a Kotlin CorDapp - kotlin-source/build/nodes/runnodes.bat - - // On Mac for a Java CorDapp - java-source/build/nodes/runnodes - - // On Mac for a Kotlin CorDapp - kotlin-source/build/nodes/runnodes + // On Mac + build/nodes/runnodes This will start a terminal window for each node, and an additional terminal window for each node's webserver - eight terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays @@ -138,10 +126,8 @@ We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing: start IOUFlow iouValue: 99, otherParty: "NodeB" -Node A and Node B will automatically agree an IOU. - -If the flow worked, it should have led to the recording of a new IOU in the vaults of both Node A and Node B. Equally -importantly, Node C - although it sits on the same network - should not be aware of this transaction. +Node A and Node B will automatically agree an IOU. If the flow worked, it should have led to the recording of a new IOU +in the vaults of both Node A and Node B. We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run`` will display a list of the available commands. We can examine the contents of a node's vault by running: @@ -166,8 +152,7 @@ The vaults of Node A and Node B should both display the following output: value: 99 lender: "CN=NodeA,O=NodeA,L=London,C=GB" borrower: "CN=NodeB,O=NodeB,L=New York,C=US" - contract: - legalContractReference: "559322B95BCF7913E3113962DC3F3CBD71C818C66977721580C045DC41C813A5" + contract: {} participants: - "CN=NodeA,O=NodeA,L=London,C=GB" - "CN=NodeB,O=NodeB,L=New York,C=US" @@ -178,13 +163,6 @@ The vaults of Node A and Node B should both display the following output: index: 0 second: "(observable)" -But the vault of Node C should output nothing! - -.. code:: python - - first: [] - second: "(observable)" - Conclusion ---------- We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Like all CorDapps, our diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst index 3888d07ee9..a4ba0ef205 100644 --- a/docs/source/hello-world-state.rst +++ b/docs/source/hello-world-state.rst @@ -64,31 +64,26 @@ you wish to add them later, its as simple as adding an additional property to yo Defining IOUState ----------------- -Let's open ``TemplateState.java`` (for Java) or ``TemplateState.kt`` (for Kotlin) and update ``TemplateState`` to +Let's open ``TemplateState.java`` (for Java) or ``App.kt`` (for Kotlin) and update ``TemplateState`` to define an ``IOUState``: .. container:: codeset .. code-block:: kotlin - package com.iou - - import net.corda.core.contracts.ContractState - import net.corda.core.identity.Party - class IOUState(val value: Int, val lender: Party, - val borrower: Party, - override val contract: TemplateContract) : ContractState { - + val borrower: Party) : ContractState { + override val contract = TemplateContract() override val participants get() = listOf(lender, borrower) } .. code-block:: java - package com.iou; + package com.template.state; import com.google.common.collect.ImmutableList; + import com.template.contract.TemplateContract; import net.corda.core.contracts.ContractState; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.Party; @@ -99,7 +94,7 @@ define an ``IOUState``: private final int value; private final Party lender; private final Party borrower; - private final IOUContract contract = new IOUContract(); + private final TemplateContract contract = new TemplateContract(); public IOUState(int value, Party lender, Party borrower) { this.value = value; @@ -121,7 +116,7 @@ define an ``IOUState``: @Override // TODO: Once we've defined IOUContract, come back and update this. - public IOUContract getContract() { + public TemplateContract getContract() { return contract; } @@ -131,6 +126,8 @@ define an ``IOUState``: } } +If you're following along in Java, you'll also need to rename ``TemplateState.java`` to ``IOUState.java``. + We've made the following changes: * We've renamed ``TemplateState`` to ``IOUState`` diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst index 05b9a31224..acb131dc6f 100644 --- a/docs/source/hello-world-template.rst +++ b/docs/source/hello-world-template.rst @@ -25,11 +25,11 @@ Open a terminal window in the directory where you want to download the CorDapp t .. code-block:: text # Clone the template from GitHub: - git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template + git clone https://github.com/corda/cordapp-template-java.git ; cd cordapp-template-java *or* - git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template + git clone https://github.com/corda/cordapp-template-kotlin.git ; cd cordapp-template-kotlin # Retrieve a list of the stable Milestone branches using: git branch -a --list *release-M* @@ -39,35 +39,26 @@ Open a terminal window in the directory where you want to download the CorDapp t Template structure ------------------ -We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. If -you want to write the CorDapp in Java, you'll be modifying the files under ``java-source``. If you prefer to use -Kotlin, you'll be modifying the files under ``kotlin-source``. - -To implement our IOU CorDapp, we'll only need to modify three files: +We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. To +implement our IOU CorDapp in Java, we'll need to modify three files. For Kotlin, we'll simply be modifying the +``App.kt`` file: .. container:: codeset .. code-block:: java // 1. The state - java-source/src/main/java/com/template/state/TemplateState.java + src/main/java/com/template/state/TemplateState.java // 2. The contract - java-source/src/main/java/com/template/contract/TemplateContract.java + src/main/java/com/template/contract/TemplateContract.java // 3. The flow - java-source/src/main/java/com/template/flow/TemplateFlow.java + src/main/java/com/template/flow/TemplateFlow.java .. code-block:: kotlin - // 1. The state - kotlin-source/src/main/kotlin/com/template/state/TemplateState.kt - - // 2. The contract - kotlin-source/src/main/kotlin/com/template/contract/TemplateContract.kt - - // 3. The flow - kotlin-source/src/main/kotlin/com/template/flow/TemplateFlow.kt + src/main/kotlin/com/template/App.kt Progress so far --------------- diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index ef6596cf20..dbc884af41 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -8,6 +8,8 @@ Unreleased * Merged handling of well known and confidential identities in the identity service. +* Remove `IssuerFlow` as it allowed nodes to request arbitrary amounts of cash to be issued from any remote node. + Milestone 14 ------------ diff --git a/docs/source/resources/simple-tutorial-transaction.png b/docs/source/resources/simple-tutorial-transaction.png index f881346b06..7b1b128dda 100644 Binary files a/docs/source/resources/simple-tutorial-transaction.png and b/docs/source/resources/simple-tutorial-transaction.png differ diff --git a/docs/source/resources/transaction-flow.png b/docs/source/resources/transaction-flow.png index 1860f80b76..f874292f49 100644 Binary files a/docs/source/resources/transaction-flow.png and b/docs/source/resources/transaction-flow.png differ diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index 884484aa95..60faacc158 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -41,7 +41,7 @@ To run from the command line in Windows: 1. Run ``gradlew samples:trader-demo:deployNodes`` to create a set of configs and installs under ``samples\trader-demo\build\nodes`` 2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes -3. Run ``gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node +3. Run ``gradlew samples:trader-demo:runBank`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node 4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` you can see flows running on both sides of transaction. Additionally you should see final trade information displayed to your terminal. diff --git a/docs/source/tut-two-party-contract.rst b/docs/source/tut-two-party-contract.rst index 71f35f60cf..077133b378 100644 --- a/docs/source/tut-two-party-contract.rst +++ b/docs/source/tut-two-party-contract.rst @@ -11,23 +11,33 @@ Remember that each state references a contract. The contract imposes constraints If the transaction does not obey the constraints of all the contracts of all its states, it cannot become a valid ledger update. -We need to modify our contract so that the lender's signature is required in any IOU creation transaction. This will -only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final line of the -``requireThat`` block as follows: +We need to modify our contract so that the borrower's signature is required in any IOU creation transaction. This will +only require changing a single line of code. In ``IOUContract.java``/``IOUContract.kt``, update the final two lines of +the ``requireThat`` block as follows: .. container:: codeset .. code-block:: kotlin + // Constraints on the signers. + "There must be two signers." using (command.signers.toSet().size == 2) "The borrower and lender must be signers." using (command.signers.containsAll(listOf( out.borrower.owningKey, out.lender.owningKey))) .. code-block:: java + ... + + import com.google.common.collect.ImmutableList; + + ... + + // Constraints on the signers. + check.using("There must be two signers.", command.getSigners().size() == 2); check.using("The borrower and lender must be signers.", command.getSigners().containsAll( ImmutableList.of(borrower.getOwningKey(), lender.getOwningKey()))); Progress so far --------------- -Our contract now imposes an additional constraint - the lender must also sign an IOU creation transaction. Next, we -need to update ``IOUFlow`` so that it actually gathers the counterparty's signature as part of the flow. \ No newline at end of file +Our contract now imposes an additional constraint - the borrower must also sign an IOU creation transaction. Next, we +need to update ``IOUFlow`` so that it actually gathers the borrower's signature as part of the flow. \ No newline at end of file diff --git a/docs/source/tut-two-party-flow.rst b/docs/source/tut-two-party-flow.rst index bd5b5a87fc..441bd43c3b 100644 --- a/docs/source/tut-two-party-flow.rst +++ b/docs/source/tut-two-party-flow.rst @@ -9,17 +9,17 @@ Updating the flow To update the flow, we'll need to do two things: -* Update the borrower's side of the flow to request the lender's signature -* Create a flow for the lender to run in response to a signature request from the borrower +* Update the lender's side of the flow to request the borrower's signature +* Create a flow for the borrower to run in response to a signature request from the lender -Updating the borrower's flow ----------------------------- +Updating the lender's flow +-------------------------- In the original CorDapp, we automated the process of notarising a transaction and recording it in every party's vault by invoking a built-in flow called ``FinalityFlow`` as a subflow. We're going to use another pre-defined flow, called -``CollectSignaturesFlow``, to gather the lender's signature. +``CollectSignaturesFlow``, to gather the borrower's signature. -We also need to add the lender's public key to the transaction's command, making the lender one of the required signers -on the transaction. +We also need to add the borrower's public key to the transaction's command, making the borrower one of the required +signers on the transaction. In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: @@ -27,6 +27,8 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: .. code-block:: kotlin + ... + // We add the items to the builder. val state = IOUState(iouValue, me, otherParty) val cmd = Command(IOUContract.Create(), listOf(me.owningKey, otherParty.owningKey)) @@ -36,16 +38,24 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: txBuilder.verify(serviceHub) // Signing the transaction. - val signedTx = serviceHub.toSignedTransaction(txBuilder) + val signedTx = serviceHub.signInitialTransaction(txBuilder) // Obtaining the counterparty's signature - val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx)) + val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, CollectSignaturesFlow.tracker())) // Finalising the transaction. subFlow(FinalityFlow(fullySignedTx)) .. code-block:: java + ... + + import com.google.common.collect.ImmutableList; + import java.security.PublicKey; + import java.util.List; + + ... + // We add the items to the builder. IOUState state = new IOUState(iouValue, me, otherParty); List requiredSigners = ImmutableList.of(me.getOwningKey(), otherParty.getOwningKey()); @@ -56,65 +66,42 @@ In ``IOUFlow.java``/``IOUFlow.kt``, update ``IOUFlow.call`` as follows: txBuilder.verify(getServiceHub()); // Signing the transaction. - final SignedTransaction signedTx = getServiceHub().toSignedTransaction(txBuilder); + final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder); // Obtaining the counterparty's signature - final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, null)); + final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(signedTx, CollectSignaturesFlow.Companion.tracker())); // Finalising the transaction. subFlow(new FinalityFlow(fullySignedTx)); -To make the lender a required signer, we simply add the lender's public key to the list of signers on the command. + return null; + +To make the borrower a required signer, we simply add the borrower's public key to the list of signers on the command. ``CollectSignaturesFlow``, meanwhile, takes a transaction signed by the flow initiator, and returns a transaction signed by all the transaction's other required signers. We then pass this fully-signed transaction into ``FinalityFlow``. -The lender's flow ------------------ -Reorganising our class -^^^^^^^^^^^^^^^^^^^^^^ -Before we define the lender's flow, let's reorganise ``IOUFlow.java``/``IOUFlow.kt`` a little bit: - -* Rename ``IOUFlow`` to ``Initiator`` -* In Java, make the ``Initiator`` class static, rename its constructor to match the new name, and move the definition - inside an enclosing ``IOUFlow`` class -* In Kotlin, move the definition of ``Initiator`` class inside an enclosing ``IOUFlow`` singleton object - -We will end up with the following structure: - -.. container:: codeset - - .. code-block:: kotlin - - object IOUFlow { - @InitiatingFlow - @StartableByRPC - class Initiator(val iouValue: Int, - val otherParty: Party) : FlowLogic() { - - .. code-block:: java - - public class IOUFlow { - @InitiatingFlow - @StartableByRPC - public static class Initiator extends FlowLogic { - -Writing the lender's flow -^^^^^^^^^^^^^^^^^^^^^^^^^ +Creating the borrower's flow +---------------------------- We're now ready to write the lender's flow, which will respond to the borrower's attempt to gather our signature. - -Inside the ``IOUFlow`` class/singleton object, add the following class: +In a new ``IOUFlowResponder.java`` file in Java, or within the ``App.kt`` file in Kotlin, add the following class: .. container:: codeset .. code-block:: kotlin - @InitiatedBy(Initiator::class) - class Acceptor(val otherParty: Party) : FlowLogic() { + ... + + import net.corda.core.transactions.SignedTransaction + + ... + + @InitiatedBy(IOUFlow::class) + class IOUFlowResponder(val otherParty: Party) : FlowLogic() { @Suspendable override fun call() { - val signTransactionFlow = object : SignTransactionFlow(otherParty) { + val signTransactionFlow = object : SignTransactionFlow(otherParty, SignTransactionFlow.tracker()) { override fun checkTransaction(stx: SignedTransaction) = requireThat { val output = stx.tx.outputs.single().data "This must be an IOU transaction." using (output is IOUState) @@ -129,12 +116,26 @@ Inside the ``IOUFlow`` class/singleton object, add the following class: .. code-block:: java - @InitiatedBy(Initiator.class) - public static class Acceptor extends FlowLogic { + package com.template.flow; + import co.paralleluniverse.fibers.Suspendable; + import com.template.state.IOUState; + import net.corda.core.contracts.ContractState; + import net.corda.core.flows.FlowException; + import net.corda.core.flows.FlowLogic; + import net.corda.core.flows.InitiatedBy; + import net.corda.core.flows.SignTransactionFlow; + import net.corda.core.identity.Party; + import net.corda.core.transactions.SignedTransaction; + import net.corda.core.utilities.ProgressTracker; + + import static net.corda.core.contracts.ContractsDSL.requireThat; + + @InitiatedBy(IOUFlow.class) + public class IOUFlowResponder extends FlowLogic { private final Party otherParty; - public Acceptor(Party otherParty) { + public IOUFlowResponder(Party otherParty) { this.otherParty = otherParty; } @@ -142,8 +143,8 @@ Inside the ``IOUFlow`` class/singleton object, add the following class: @Override public Void call() throws FlowException { class signTxFlow extends SignTransactionFlow { - private signTxFlow(Party otherParty) { - super(otherParty, null); + private signTxFlow(Party otherParty, ProgressTracker progressTracker) { + super(otherParty, progressTracker); } @Override @@ -158,18 +159,19 @@ Inside the ``IOUFlow`` class/singleton object, add the following class: } } - subFlow(new signTxFlow(otherParty)); + subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker())); return null; } } -As with the ``Initiator``, our ``Acceptor`` flow is a ``FlowLogic`` subclass where we've overridden ``FlowLogic.call``. +As with the ``IOUFlow``, our ``IOUFlowResponder`` flow is a ``FlowLogic`` subclass where we've overridden +``FlowLogic.call``. -The flow is annotated with ``InitiatedBy(Initiator.class)``, which means that your node will invoke ``Acceptor.call`` -when it receives a message from a instance of ``Initiator`` running on another node. What will this message from the -``Initiator`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that we'll be sent a -``SignedTransaction``, and are expected to send back our signature over that transaction. +The flow is annotated with ``InitiatedBy(IOUFlow.class)``, which means that your node will invoke +``IOUFlowResponder.call`` when it receives a message from a instance of ``Initiator`` running on another node. What +will this message from the ``IOUFlow`` be? If we look at the definition of ``CollectSignaturesFlow``, we can see that +we'll be sent a ``SignedTransaction``, and are expected to send back our signature over that transaction. We could handle this manually. However, there is also a pre-defined flow called ``SignTransactionFlow`` that can handle this process for us automatically. ``SignTransactionFlow`` is an abstract class, and we must subclass it and override @@ -179,7 +181,7 @@ Once we've defined the subclass, we invoke it using ``FlowLogic.subFlow``, and t and the lender's flow is conducted automatically. CheckTransactions -~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^ ``SignTransactionFlow`` will automatically verify the transaction and its signatures before signing it. However, just because a transaction is valid doesn't mean we necessarily want to sign. What if we don't want to deal with the counterparty in question, or the value is too high, or we're not happy with the transaction's structure? @@ -200,4 +202,4 @@ We can now run our updated CorDapp, using the instructions :doc:`here `_ -with an API and web front-end, and a set of example CorDapps in -`the main Corda repo `_, under ``samples``. An explanation of how to run these -samples :doc:`here `. - -As you write CorDapps, you can learn more about the API available :doc:`here `. - -If you get stuck at any point, please reach out on `Slack `_, -`Discourse `_, or `Stack Overflow `_. \ No newline at end of file diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 517bd1ca66..0181e0778b 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -132,7 +132,7 @@ When starting a standalone node using a configuration file we must supply the RP .. code-block:: text rpcUsers : [ - { username=user, password=password, permissions=[ StartFlow.net.corda.flows.CashFlow ] } + { username=user, password=password, permissions=[ StartFlow.net.corda.finance.flows.CashFlow ] } ] When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar @@ -143,7 +143,7 @@ manner: rpcUsers = [ ['username' : "user", 'password' : "password", - 'permissions' : ["StartFlow.net.corda.flows.CashFlow"]] + 'permissions' : ["StartFlow.net.corda.finance.flows.CashFlow"]] ] You can then deploy and launch the nodes (Notary and Alice) as follows: diff --git a/docs/source/tutorial-contract-clauses.rst b/docs/source/tutorial-contract-clauses.rst index c8b3ca98cd..97ad1ba675 100644 --- a/docs/source/tutorial-contract-clauses.rst +++ b/docs/source/tutorial-contract-clauses.rst @@ -66,8 +66,6 @@ We start by defining the ``CommercialPaper`` class. As in the previous tutorial, .. sourcecode:: kotlin class CommercialPaper : Contract { - override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") - override fun verify(tx: LedgerTransaction) = verifyClause(tx, Clauses.Group(), tx.commands.select()) interface Commands : CommandData { @@ -79,11 +77,6 @@ We start by defining the ``CommercialPaper`` class. As in the previous tutorial, .. sourcecode:: java public class CommercialPaper implements Contract { - @Override - public SecureHash getLegalContractReference() { - return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - } - @Override public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException { ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx)); diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index cdee7aa768..1cffea1982 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -59,8 +59,6 @@ Kotlin syntax works. .. sourcecode:: kotlin class CommercialPaper : Contract { - override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - override fun verify(tx: LedgerTransaction) { TODO() } @@ -69,22 +67,16 @@ Kotlin syntax works. .. sourcecode:: java public class CommercialPaper implements Contract { - @Override - public SecureHash getLegalContractReference() { - return SecureHash.Companion.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - } - @Override public void verify(LedgerTransaction tx) { throw new UnsupportedOperationException(); } } -Every contract must have at least a ``getLegalContractReference()`` and a ``verify()`` method. In Kotlin we express -a getter without a setter as an immutable property (val). The *legal contract reference* is supposed to be a hash -of a document that describes the legal contract and may take precedence over the code, in case of a dispute. +Every contract must have at least a ``verify()`` method. -.. note:: The way legal contract prose is bound to a smart contract implementation will change in future. +.. note:: In the future there will be a way to bind legal contract prose to a smart contract implementation, + that may take precedence over the code in case of a dispute. The verify method returns nothing. This is intentional: the function either completes correctly, or throws an exception, in which case the transaction is rejected. diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 7ab46f30c1..7b96013513 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -407,10 +407,10 @@ list: .. sourcecode:: none com.example.flow.ExampleFlow$Initiator - net.corda.flows.CashExitFlow - net.corda.flows.CashIssueFlow - net.corda.flows.CashPaymentFlow - net.corda.flows.ContractUpgradeFlow + net.corda.finance.flows.CashExitFlow + net.corda.finance.flows.CashIssueFlow + net.corda.finance.flows.CashPaymentFlow + net.corda.finance.flows.ContractUpgradeFlow We can create a new IOU using the ``ExampleFlow$Initiator`` flow. For example, from the interactive shell of NodeA, you can agree an IOU of 50 with NodeB by running ``flow start Initiator iouValue: 50, otherParty: NodeB``. @@ -450,8 +450,7 @@ We can see a list of the states in our node's vault using ``run vaultAndUpdates` linearId: externalId: null id: "84628565-2688-45ef-bb06-aae70fcf3be7" - contract: - legalContractReference: "4DDE2A47C361106CBAEC06CC40FE418A994822A3C8054851FEECD51207BFAF82" + contract: {} participants: - "CN=NodeB,O=NodeB,L=New York,C=US" - "CN=NodeA,O=NodeA,L=London,C=UK" @@ -485,8 +484,7 @@ abbreviated the output below): linearId: externalId: null id: "84628565-2688-45ef-bb06-aae70fcf3be7" - contract: - legalContractReference: "4DDE2A47C361106CBAEC06CC40FE418A994822A3C8054851FEECD51207BFAF82" + contract: {} participants: - "CN=NodeB,O=NodeB,L=New York,C=US" - "CN=NodeA,O=NodeA,L=London,C=UK" diff --git a/docs/source/using-a-notary.rst b/docs/source/using-a-notary.rst index 08517c6eaa..2c9f5c2fef 100644 --- a/docs/source/using-a-notary.rst +++ b/docs/source/using-a-notary.rst @@ -87,9 +87,9 @@ we just use a helper that handles it for us. We also assume that we already have .. sourcecode:: kotlin val inputState = StateAndRef(sate, stateRef) - val moveTransactionBuilder = DummyContract.move(inputState, newOwner = aliceParty.owningKey) + val moveTransactionBuilder = DummyContract.withNewOwnerAndAmount(inputState, newOwner = aliceParty.owningKey) -The ``DummyContract.move()`` method will a new transaction builder with our old state as the input, a new state +The ``DummyContract.withNewOwnerAndAmount()`` method will a new transaction builder with our old state as the input, a new state with Alice as the owner, and a relevant contract command for "move". Again we sign the transaction, and build it: diff --git a/docs/source/writing-cordapps.rst b/docs/source/writing-cordapps.rst index c63be2b40f..e99bd03e70 100644 --- a/docs/source/writing-cordapps.rst +++ b/docs/source/writing-cordapps.rst @@ -92,6 +92,8 @@ The ``WebServerPluginRegistry`` class defines the following: be distributed within the CorDapp jars. This static content will not be available if the bundled web server is not started +* ``customizeJSONSerialization``, which can be overridden to register custom JSON serializers if required by the REST api. + * The static web content itself should be placed inside the ``src/main/resources`` directory To learn about how to use gradle to build your cordapp against Corda and generate an artifact please read diff --git a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowSnapshotTreeDataManager.kt b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowSnapshotTreeDataManager.kt index d4d6f4ae05..1a933361ff 100644 --- a/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowSnapshotTreeDataManager.kt +++ b/experimental/intellij-plugin/src/main/kotlin/net/corda/ideaplugin/toolwindow/FlowSnapshotTreeDataManager.kt @@ -80,7 +80,7 @@ class FlowSnapshotTreeDataManager(tree: JTree) { private fun insertNodeToSnapshotModel(snapshotFile: File, insertionIndex: Int = -1) { val fileNode = DefaultMutableTreeNode(Descriptor(snapshotFile, AllIcons.FileTypes.Custom, snapshotFile.name)) val snapshot = mapper.readValue(snapshotFile, FlowStackSnapshot::class.java) - fileNode.add(DefaultMutableTreeNode(Descriptor(snapshot.timestamp, AllIcons.Debugger.Db_primitive, "timestamp"))) + fileNode.add(DefaultMutableTreeNode(Descriptor(snapshot.time, AllIcons.Debugger.Db_primitive, "timestamp"))) fileNode.add(DefaultMutableTreeNode(Descriptor(snapshot.flowClass, AllIcons.Debugger.Db_primitive, "flowClass"))) val framesNode = DefaultMutableTreeNode(Descriptor(icon = AllIcons.Debugger.Db_array, label = "stackFrames")) fileNode.add(framesNode) diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Arrangement.kt similarity index 95% rename from experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/Arrangement.kt index 73d7737eaa..5d1c67eaf4 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Arrangement.kt @@ -1,8 +1,8 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.Frequency import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable +import net.corda.finance.contracts.Frequency import java.math.BigDecimal import java.time.LocalDate import java.util.* diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/ContractFunctions.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/ContractFunctions.kt similarity index 96% rename from experimental/src/main/kotlin/net/corda/contracts/universal/ContractFunctions.kt rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/ContractFunctions.kt index 865240c09b..49c0ebb86f 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/ContractFunctions.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/ContractFunctions.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal import net.corda.core.identity.Party import java.math.BigDecimal diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Literal.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Literal.kt similarity index 97% rename from experimental/src/main/kotlin/net/corda/contracts/universal/Literal.kt rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/Literal.kt index 9d75a4ed30..8da331cf32 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Literal.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Literal.kt @@ -1,8 +1,8 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.BusinessCalendar -import net.corda.contracts.Frequency import net.corda.core.identity.Party +import net.corda.finance.contracts.BusinessCalendar +import net.corda.finance.contracts.Frequency import java.math.BigDecimal import java.time.LocalDate import java.util.* diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Perceivable.kt similarity index 98% rename from experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/Perceivable.kt index f5d25d5031..af72738962 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Perceivable.kt @@ -1,9 +1,9 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.BusinessCalendar -import net.corda.contracts.Tenor import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable +import net.corda.finance.contracts.BusinessCalendar +import net.corda.finance.contracts.Tenor import java.lang.reflect.Type import java.math.BigDecimal import java.security.PublicKey diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/PrettyPrint.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/PrettyPrint.kt similarity index 99% rename from experimental/src/main/kotlin/net/corda/contracts/universal/PrettyPrint.kt rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/PrettyPrint.kt index efb1e499e0..615316a6bb 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/PrettyPrint.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/PrettyPrint.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal import net.corda.core.crypto.commonName import net.corda.core.crypto.toStringShort diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/README.md b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/README.md similarity index 100% rename from experimental/src/main/kotlin/net/corda/contracts/universal/README.md rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/README.md diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt similarity index 97% rename from experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt index 0e695837ef..d438326e7b 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/UniversalContract.kt @@ -1,13 +1,12 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.BusinessCalendar -import net.corda.contracts.FixOf import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.BusinessCalendar +import net.corda.finance.contracts.FixOf import java.math.BigDecimal import java.time.Instant @@ -21,7 +20,7 @@ class UniversalContract : Contract { interface Commands : CommandData { - data class Fix(val fixes: List) : Commands + data class Fix(val fixes: List) : Commands // transition according to business rules defined in contract data class Action(val name: String) : Commands @@ -316,9 +315,6 @@ class UniversalContract : Contract { else -> throw NotImplementedError("replaceFixing - " + arr.javaClass.name) } - override val legalContractReference: SecureHash - get() = throw UnsupportedOperationException() - fun generateIssue(tx: TransactionBuilder, arrangement: Arrangement, at: PartyAndReference, notary: Party) { check(tx.inputStates().isEmpty()) tx.addOutputState(State(listOf(notary), arrangement)) diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Util.kt b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Util.kt similarity index 99% rename from experimental/src/main/kotlin/net/corda/contracts/universal/Util.kt rename to experimental/src/main/kotlin/net/corda/finance/contracts/universal/Util.kt index c07257802b..65aee09b89 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Util.kt +++ b/experimental/src/main/kotlin/net/corda/finance/contracts/universal/Util.kt @@ -1,9 +1,9 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal import com.google.common.collect.ImmutableSet import com.google.common.collect.Sets -import net.corda.contracts.Frequency import net.corda.core.identity.Party +import net.corda.finance.contracts.Frequency import java.security.PublicKey import java.time.Instant import java.time.LocalDate diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt similarity index 87% rename from experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt index c4c8767d95..e4fa27ae78 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Cap.kt @@ -1,9 +1,9 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.BusinessCalendar -import net.corda.contracts.FixOf -import net.corda.contracts.Frequency -import net.corda.contracts.Tenor +import net.corda.finance.contracts.BusinessCalendar +import net.corda.finance.contracts.FixOf +import net.corda.finance.contracts.Frequency +import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore @@ -196,32 +196,32 @@ class Cap { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } this.verifies() } @@ -280,32 +280,32 @@ class Cap { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } this.verifies() } diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt similarity index 85% rename from experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt index 0e475216a3..99d75414f9 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Caplet.kt @@ -1,7 +1,7 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.FixOf -import net.corda.contracts.Tenor +import net.corda.finance.contracts.FixOf +import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore @@ -101,32 +101,32 @@ class Caplet { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))) } this.verifies() } diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ContractDefinition.kt similarity index 98% rename from experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/ContractDefinition.kt index e01f989b8d..75924ff0e0 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ContractDefinition.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ContractDefinition.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Examples.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Examples.kt similarity index 97% rename from experimental/src/test/kotlin/net/corda/contracts/universal/Examples.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/Examples.kt index f41f81b9bf..e4b989a6bf 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Examples.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Examples.kt @@ -1,6 +1,6 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.core.contracts.USD +import net.corda.finance.USD import org.junit.Ignore import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt similarity index 99% rename from experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt index 51c2ab4dd3..df72fddfa5 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXFwdTimeOption.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt similarity index 99% rename from experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt index e3cba5fdd7..ae9a4b822e 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/FXSwap.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt similarity index 91% rename from experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt index abdc4bb445..e64850cf99 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/IRS.kt @@ -1,8 +1,8 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.FixOf -import net.corda.contracts.Frequency -import net.corda.contracts.Tenor +import net.corda.finance.contracts.FixOf +import net.corda.finance.contracts.Frequency +import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore @@ -163,32 +163,32 @@ class IRS { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.finance.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } this.verifies() } diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt similarity index 98% rename from experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt index aeccd75e75..4db190640d 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/RollOutTests.kt @@ -1,6 +1,6 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.Frequency +import net.corda.finance.contracts.Frequency import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt similarity index 94% rename from experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt index e71bd6746c..5a29086fae 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/Swaption.kt @@ -1,7 +1,7 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal -import net.corda.contracts.Frequency -import net.corda.contracts.Tenor +import net.corda.finance.contracts.Frequency +import net.corda.finance.contracts.Tenor import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt similarity index 98% rename from experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt rename to experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt index 3f41d0c260..7590505327 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/finance/contracts/universal/ZeroCouponBond.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.universal +package net.corda.finance.contracts.universal import net.corda.testing.DUMMY_NOTARY import net.corda.testing.transaction diff --git a/finance/build.gradle b/finance/build.gradle index 9604e46672..f4d43eeff7 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -5,13 +5,17 @@ apply plugin: 'kotlin-jpa' apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' +apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'com.jfrog.artifactory' description 'Corda finance modules' dependencies { - compile project(':core') - compile project(':node-schemas') + // Note the :finance module is a CorDapp in its own right + // and CorDapps using :finance features should use 'cordapp' not 'compile' linkage. + cordaCompile project(':core') + + compile "com.google.guava:guava:$guava_version" testCompile project(':test-utils') testCompile project(path: ':core', configuration: 'testArtifacts') @@ -37,4 +41,4 @@ jar { publish { name jar.baseName -} \ No newline at end of file +} diff --git a/finance/isolated/src/main/kotlin/net/corda/contracts/isolated/AnotherDummyContract.kt b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/AnotherDummyContract.kt similarity index 84% rename from finance/isolated/src/main/kotlin/net/corda/contracts/isolated/AnotherDummyContract.kt rename to finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/AnotherDummyContract.kt index 9d615cc02b..9e1d5bf6a2 100644 --- a/finance/isolated/src/main/kotlin/net/corda/contracts/isolated/AnotherDummyContract.kt +++ b/finance/isolated/src/main/kotlin/net/corda/finance/contracts/isolated/AnotherDummyContract.kt @@ -1,7 +1,6 @@ -package net.corda.contracts.isolated +package net.corda.finance.contracts.isolated import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction @@ -25,9 +24,6 @@ class AnotherDummyContract : Contract, DummyContractBackdoor { // Always accepts. } - // The "empty contract" - override val legalContractReference: SecureHash = SecureHash.sha256("https://anotherdummy.org") - override fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { val state = State(magicNumber) return TransactionBuilder(notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) diff --git a/finance/src/main/java/net/corda/contracts/ICommercialPaperState.java b/finance/src/main/java/net/corda/finance/contracts/ICommercialPaperState.java similarity index 77% rename from finance/src/main/java/net/corda/contracts/ICommercialPaperState.java rename to finance/src/main/java/net/corda/finance/contracts/ICommercialPaperState.java index 0923897593..e60b2c30d5 100644 --- a/finance/src/main/java/net/corda/contracts/ICommercialPaperState.java +++ b/finance/src/main/java/net/corda/finance/contracts/ICommercialPaperState.java @@ -1,11 +1,12 @@ -package net.corda.contracts; +package net.corda.finance.contracts; -import net.corda.core.contracts.*; +import net.corda.core.contracts.Amount; +import net.corda.core.contracts.ContractState; +import net.corda.core.contracts.Issued; import net.corda.core.identity.AbstractParty; -import java.security.PublicKey; -import java.time.*; -import java.util.*; +import java.time.Instant; +import java.util.Currency; /* This is an interface solely created to demonstrate that the same kotlin tests can be run against * either a Java implementation of the CommercialPaper or a kotlin implementation. diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java similarity index 93% rename from finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java rename to finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java index d4910e67a9..2182999e56 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/finance/contracts/JavaCommercialPaper.java @@ -1,14 +1,10 @@ -package net.corda.contracts; +package net.corda.finance.contracts; import co.paralleluniverse.fibers.Suspendable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import kotlin.Pair; import kotlin.Unit; -import net.corda.contracts.asset.Cash; -import net.corda.contracts.asset.CashKt; import net.corda.core.contracts.*; -import net.corda.core.crypto.SecureHash; import net.corda.core.crypto.testing.NullPublicKey; import net.corda.core.identity.AbstractParty; import net.corda.core.identity.AnonymousParty; @@ -16,11 +12,15 @@ import net.corda.core.identity.Party; import net.corda.core.node.ServiceHub; import net.corda.core.transactions.LedgerTransaction; import net.corda.core.transactions.TransactionBuilder; +import net.corda.finance.contracts.asset.Cash; +import net.corda.finance.utils.StateSumming; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.time.Instant; -import java.util.*; +import java.util.Collections; +import java.util.Currency; +import java.util.List; import java.util.stream.Collectors; import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; @@ -83,7 +83,7 @@ public class JavaCommercialPaper implements Contract { return owner; } - public Amount> getFaceValue() { + Amount> getFaceValue() { return faceValue; } @@ -121,7 +121,7 @@ public class JavaCommercialPaper implements Contract { return result; } - public State withoutOwner() { + State withoutOwner() { return new State(issuance, new AnonymousParty(NullPublicKey.INSTANCE), faceValue, maturityDate); } @@ -173,9 +173,7 @@ public class JavaCommercialPaper implements Contract { // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming // it for cash on or after the maturity date. final List> commands = tx.getCommands().stream().filter( - it -> { - return it.getValue() instanceof Commands; - } + it -> it.getValue() instanceof Commands ).collect(Collectors.toList()); final AuthenticatedObject command = Iterables.getOnlyElement(commands); final TimeWindow timeWindow = tx.getTimeWindow(); @@ -207,7 +205,7 @@ public class JavaCommercialPaper implements Contract { final Instant time = null == timeWindow ? null : timeWindow.getUntilTime(); - final Amount> received = CashKt.sumCashBy(tx.getOutputs().stream().map(TransactionState::getData).collect(Collectors.toList()), input.getOwner()); + final Amount> received = StateSumming.sumCashBy(tx.getOutputStates(), input.getOwner()); requireThat(require -> { require.using("must be timestamped", timeWindow != null); @@ -237,13 +235,6 @@ public class JavaCommercialPaper implements Contract { } } - @NotNull - @Override - public SecureHash getLegalContractReference() { - // TODO: Should return hash of the contract's contents, not its URI - return SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - } - public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary, Integer encumbrance) { State state = new State(issuance, issuance.getParty(), faceValue, maturityDate); TransactionState output = new TransactionState<>(state, notary, encumbrance); @@ -256,7 +247,7 @@ public class JavaCommercialPaper implements Contract { @Suspendable public void generateRedeem(TransactionBuilder tx, StateAndRef paper, ServiceHub services) throws InsufficientBalanceException { - Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), Collections.EMPTY_SET); + Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), Collections.emptySet()); tx.addInputState(paper); tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey())); } diff --git a/finance/src/main/kotlin/net/corda/finance/CurrencyUtils.kt b/finance/src/main/kotlin/net/corda/finance/CurrencyUtils.kt new file mode 100644 index 0000000000..a440ba7ac6 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/CurrencyUtils.kt @@ -0,0 +1,33 @@ +@file:JvmName("CurrencyUtils") + +package net.corda.finance + +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.PartyAndReference +import java.math.BigDecimal +import java.util.* + +@JvmField val USD: Currency = Currency.getInstance("USD") +@JvmField val GBP: Currency = Currency.getInstance("GBP") +@JvmField val EUR: Currency = Currency.getInstance("EUR") +@JvmField val CHF: Currency = Currency.getInstance("CHF") +@JvmField val JPY: Currency = Currency.getInstance("JPY") +@JvmField val RUB: Currency = Currency.getInstance("RUB") + +fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) +fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) +fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) +fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) +fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) +fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) + +val Int.DOLLARS: Amount get() = DOLLARS(this) +val Double.DOLLARS: Amount get() = DOLLARS(this) +val Int.POUNDS: Amount get() = POUNDS(this) +val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) + +infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) +infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit)) diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt similarity index 94% rename from finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt index f94ff09dd5..c05cc175a6 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/CommercialPaper.kt @@ -1,10 +1,7 @@ -package net.corda.contracts +package net.corda.finance.contracts import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty @@ -16,7 +13,9 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.schemas.CommercialPaperSchemaV1 +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.schemas.CommercialPaperSchemaV1 +import net.corda.finance.utils.sumCashBy import java.time.Instant import java.util.* @@ -45,9 +44,6 @@ val CP_PROGRAM_ID = CommercialPaper() // TODO: Generalise the notion of an owned instrument into a superclass/supercontract. Consider composition vs inheritance. class CommercialPaper : Contract { - // TODO: should reference the content of the legal agreement, not its URI - override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper") - data class State( val issuance: PartyAndReference, override val owner: AbstractParty, @@ -88,6 +84,9 @@ class CommercialPaper : Contract { else -> throw IllegalArgumentException("Unrecognised schema $schema") } } + + /** @suppress */ infix fun `owned by`(owner: AbstractParty) = copy(owner = owner) + /** @suppress */ infix fun `with notary`(notary: Party) = TransactionState(this, notary) } interface Commands : CommandData { @@ -191,8 +190,4 @@ class CommercialPaper : Contract { tx.addInputState(paper) tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey) } -} - -infix fun CommercialPaper.State.`owned by`(owner: AbstractParty) = copy(owner = owner) -infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary) -infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner) \ No newline at end of file +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt similarity index 99% rename from finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt index c4f4fe6546..a2d4538aa7 100644 --- a/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt @@ -1,4 +1,4 @@ -package net.corda.contracts +package net.corda.finance.contracts import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser @@ -8,7 +8,6 @@ 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 net.corda.contracts.asset.CommodityContract import net.corda.core.contracts.CommandData import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef @@ -17,6 +16,7 @@ import net.corda.core.identity.Party import net.corda.core.node.services.ServiceType import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.asset.CommodityContract import java.math.BigDecimal import java.time.DayOfWeek import java.time.LocalDate diff --git a/finance/src/main/kotlin/net/corda/contracts/GetBalances.kt b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt similarity index 97% rename from finance/src/main/kotlin/net/corda/contracts/GetBalances.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt index d1245403a5..7f6c84010d 100644 --- a/finance/src/main/kotlin/net/corda/contracts/GetBalances.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt @@ -1,6 +1,6 @@ @file:JvmName("GetBalances") -package net.corda.contracts +package net.corda.finance.contracts import net.corda.core.contracts.Amount import net.corda.core.contracts.FungibleAsset @@ -12,7 +12,7 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.builder -import net.corda.schemas.CashSchemaV1 +import net.corda.finance.schemas.CashSchemaV1 import java.util.* import kotlin.collections.LinkedHashMap diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt similarity index 51% rename from finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt index 02832f6893..792e8e6fdb 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Cash.kt @@ -1,37 +1,33 @@ -package net.corda.contracts.asset +@file:JvmName("CashUtilities") // So the static extension functions get put into a class with a better name than CashKt +package net.corda.finance.contracts.asset import co.paralleluniverse.fibers.Suspendable -import co.paralleluniverse.strands.Strand +import net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash +import net.corda.core.contracts.Amount.Companion.sumOrThrow import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.internal.Emoji import net.corda.core.node.ServiceHub -import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.deserialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.toHexString -import net.corda.core.utilities.toNonEmptySet -import net.corda.core.utilities.trace -import net.corda.schemas.CashSchemaV1 +import net.corda.finance.schemas.CashSchemaV1 +import net.corda.finance.utils.sumCash +import net.corda.finance.utils.sumCashOrNull +import net.corda.finance.utils.sumCashOrZero import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.PublicKey -import java.sql.SQLException +import java.sql.DatabaseMetaData import java.util.* -import java.util.concurrent.locks.ReentrantLock -import kotlin.concurrent.withLock +import java.util.concurrent.atomic.AtomicReference ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // @@ -41,6 +37,61 @@ import kotlin.concurrent.withLock // Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode. val CASH_PROGRAM_ID = Cash() +/** + * Pluggable interface to allow for different cash selection provider implementations + * Default implementation [CashSelectionH2Impl] uses H2 database and a custom function within H2 to perform aggregation. + * Custom implementations must implement this interface and declare their implementation in + * META-INF/services/net.corda.contracts.asset.CashSelection + */ +interface CashSelection { + companion object { + val instance = AtomicReference() + + fun getInstance(metadata: () -> java.sql.DatabaseMetaData): CashSelection { + return instance.get() ?: { + val _metadata = metadata() + val cashSelectionAlgos = ServiceLoader.load(CashSelection::class.java).toList() + val cashSelectionAlgo = cashSelectionAlgos.firstOrNull { it.isCompatible(_metadata) } + cashSelectionAlgo?.let { + instance.set(cashSelectionAlgo) + cashSelectionAlgo + } ?: throw ClassNotFoundException("\nUnable to load compatible cash selection algorithm implementation for JDBC driver ($_metadata)." + + "\nPlease specify an implementation in META-INF/services/net.corda.finance.contracts.asset.CashSelection") + }.invoke() + } + } + + /** + * Upon dynamically loading configured Cash Selection algorithms declared in META-INF/services + * this method determines whether the loaded implementation is compatible and usable with the currently + * loaded JDBC driver. + * Note: the first loaded implementation to pass this check will be used at run-time. + */ + fun isCompatible(metadata: DatabaseMetaData): Boolean + + /** + * Query to gather Cash states that are available + * @param services The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, + * otherwise the set of eligible states wil be filtered to only include those from these issuers. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param withIssuerRefs If not empty the specific set of issuer references to match against. + * @return The matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + @Suspendable + fun unconsumedCashStatesForSpending(services: ServiceHub, + amount: Amount, + onlyFromIssuerParties: Set = emptySet(), + notary: Party? = null, + lockId: UUID, + withIssuerRefs: Set = emptySet()): List> +} + /** * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour @@ -55,20 +106,6 @@ val CASH_PROGRAM_ID = Cash() * vaults can ignore the issuer/depositRefs and just examine the amount fields. */ class Cash : OnLedgerAsset() { - /** - * TODO: - * 1) hash should be of the contents, not the URI - * 2) allow the content to be specified at time of instance creation? - * - * Motivation: it's the difference between a state object referencing a programRef, which references a - * legalContractReference and a state object which directly references both. The latter allows the legal wording - * to evolve without requiring code changes. But creates a risk that users create objects governed by a program - * that is inconsistent with the legal contract. - */ - // DOCSTART 2 - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - - // DOCEND 2 override fun extractCommands(commands: Collection>): List> = commands.select() @@ -87,18 +124,22 @@ class Cash : OnLedgerAsset() { override val contract = CASH_PROGRAM_ID override val participants = listOf(owner) - override fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset + override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)" override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Move(), copy(owner = newOwner)) + fun ownedBy(owner: AbstractParty) = copy(owner = owner) + fun issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) + fun issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) + fun withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) /** Object Relational Mapping support. */ override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is CashSchemaV1 -> CashSchemaV1.PersistentCashState( - owner = this.owner.owningKey.toBase58String(), + owner = this.owner, pennies = this.amount.quantity, currency = this.amount.token.product.currencyCode, issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(), @@ -116,27 +157,26 @@ class Cash : OnLedgerAsset() { // DOCEND 1 // Just for grouping - interface Commands : FungibleAsset.Commands { + interface Commands : CommandData { /** * A command stating that money has been moved, optionally to fulfil another contract. * - * @param contractHash the contract this move is for the attention of. Only that contract's verify function + * @param contract the contract this move is for the attention of. Only that contract's verify function * should take the moved states into account when considering whether it is valid. Typically this will be * null. */ - data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands + data class Move(override val contract: Class? = null) : MoveCommand /** - * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction - * has a unique ID even when there are no inputs. + * Allows new cash states to be issued into existence. */ - data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands + class Issue : TypeOnlyCommandData() /** * A command stating that money has been withdrawn from the shared ledger and is now accounted for * in some other way. */ - data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit + data class Exit(val amount: Amount>) : CommandData } /** @@ -149,13 +189,12 @@ class Cash : OnLedgerAsset() { * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) - = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) + = generateIssue(tx, TransactionState(State(amount, owner), notary), Commands.Issue()) override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) - override fun generateIssueCommand() = Commands.Issue() override fun generateMoveCommand() = Commands.Move() override fun verify(tx: LedgerTransaction) { @@ -217,7 +256,6 @@ class Cash : OnLedgerAsset() { val outputAmount = outputs.sumCash() val cashCommands = tx.commands.select() requireThat { - "the issue command has a nonce" using (issueCommand.value.nonce != 0L) // TODO: This doesn't work with the trader demo, so use the underlying key instead // "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties) "output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers) @@ -227,10 +265,6 @@ class Cash : OnLedgerAsset() { } companion object { - // coin selection retry loop counter, sleep (msecs) and lock for selecting states - private val MAX_RETRIES = 5 - private val RETRY_SLEEP = 100 - private val spendLock: ReentrantLock = ReentrantLock() /** * Generate a transaction that moves an amount of currency to the given pubkey. * @@ -257,165 +291,54 @@ class Cash : OnLedgerAsset() { amount: Amount, to: AbstractParty, onlyFromParties: Set = emptySet()): Pair> { + return generateSpend(services, tx, listOf(PartyAndAmount(to, amount)), onlyFromParties) + } + /** + * Generate a transaction that moves an amount of currency to the given pubkey. + * + * Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset] + * + * @param services The [ServiceHub] to provide access to the database session. + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param amount How much currency to send. + * @param to a key of the recipient. + * @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set + * of given parties. This can be useful if the party you're trying to pay has expectations + * about which type of asset claims they are willing to accept. + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @JvmStatic + @Throws(InsufficientBalanceException::class) + @Suspendable + fun generateSpend(services: ServiceHub, + tx: TransactionBuilder, + payments: List>, + onlyFromParties: Set = emptySet()): Pair> { fun deriveState(txState: TransactionState, amt: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amt, owner = owner)) // Retrieve unspent and unlocked cash states that meet our spending criteria. - val acceptableCoins = Cash.unconsumedCashStatesForSpending(services, amount, onlyFromParties, tx.notary, tx.lockId) - return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins, + val totalAmount = payments.map { it.amount }.sumOrThrow() + val cashSelection = CashSelection.getInstance({ services.jdbcSession().metaData }) + val acceptableCoins = cashSelection.unconsumedCashStatesForSpending(services, totalAmount, onlyFromParties, tx.notary, tx.lockId) + return OnLedgerAsset.generateSpend(tx, payments, acceptableCoins, { state, quantity, owner -> deriveState(state, quantity, owner) }, { Cash().generateMoveCommand() }) - - } - - /** - * An optimised query to gather Cash states that are available and retry if they are temporarily unavailable. - * @param services The service hub to allow access to the database session - * @param amount The amount of currency desired (ignoring issues, but specifying the currency) - * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, - * otherwise the set of eligible states wil be filtered to only include those from these issuers. - * @param notary If null the notary source is ignored, if specified then only states marked - * with this notary are included. - * @param lockId The [FlowLogic.runId.uuid] of the flow, which is used to soft reserve the states. - * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. - * @param withIssuerRefs If not empty the specific set of issuer references to match against. - * @return The matching states that were found. If sufficient funds were found these will be locked, - * otherwise what is available is returned unlocked for informational purposes. - */ - @JvmStatic - @Suspendable - fun unconsumedCashStatesForSpending(services: ServiceHub, - amount: Amount, - onlyFromIssuerParties: Set = emptySet(), - notary: Party? = null, - lockId: UUID, - withIssuerRefs: Set = emptySet()): List> { - - val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1) - val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1) - - val stateAndRefs = mutableListOf>() - - // TODO: Need to provide a database provider independent means of performing this function. - // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: - // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the - // running total of such an accumulator - // 2) H2 uses session variables to perform this accumulator function: - // http://www.h2database.com/html/functions.html#set - // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) - - for (retryCount in 1..MAX_RETRIES) { - - spendLock.withLock { - val statement = services.jdbcSession().createStatement() - try { - statement.execute("CALL SET(@t, 0);") - - // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) - // the softLockReserve update will detect whether we try to lock states locked by others - val selectJoin = """ - SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id - FROM vault_states AS vs, contract_cash_states AS ccs - WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index - AND vs.state_status = 0 - AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} - AND (vs.lock_id = '$lockId' OR vs.lock_id is null) - """ + - (if (notary != null) - " AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") + - (if (onlyFromIssuerParties.isNotEmpty()) - " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + - (if (withIssuerRefs.isNotEmpty()) - " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") - - // Retrieve spendable state refs - val rs = statement.executeQuery(selectJoin) - stateAndRefs.clear() - log.debug(selectJoin) - var totalPennies = 0L - while (rs.next()) { - val txHash = SecureHash.parse(rs.getString(1)) - val index = rs.getInt(2) - val stateRef = StateRef(txHash, index) - val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) - val pennies = rs.getLong(4) - totalPennies = rs.getLong(5) - val rowLockId = rs.getString(6) - stateAndRefs.add(StateAndRef(state, stateRef)) - log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } - } - - if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { - // we should have a minimum number of states to satisfy our selection `amount` criteria - log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") - - // With the current single threaded state machine available states are guaranteed to lock. - // TODO However, we will have to revisit these methods in the future multi-threaded. - services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) - return stateAndRefs - } - log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") - // retry as more states may become available - } catch (e: SQLException) { - log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] - $e. - """) - } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine - stateAndRefs.clear() - log.warn(e.message) - // retry only if there are locked states that may become available again (or consumed with change) - } finally { - statement.close() - } - } - - log.warn("Coin selection failed on attempt $retryCount") - // TODO: revisit the back off strategy for contended spending. - if (retryCount != MAX_RETRIES) { - Strand.sleep(RETRY_SLEEP * retryCount.toLong()) - } - } - - log.warn("Insufficient spendable states identified for $amount") - return stateAndRefs } } - } // Small DSL extensions. -/** - * Sums the cash states in the list belonging to a single owner, throwing an exception - * if there are none, or if any of the cash states cannot be added together (i.e. are - * different currencies or issuers). - */ -fun Iterable.sumCashBy(owner: AbstractParty): Amount> = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() - -/** - * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash - * states cannot be added together (i.e. are different currencies or issuers). - */ -fun Iterable.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() - -/** Sums the cash states in the list, returning null if there are none. */ -fun Iterable.sumCashOrNull(): Amount>? = filterIsInstance().map { it.amount }.sumOrNull() - -/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */ -fun Iterable.sumCashOrZero(currency: Issued): Amount> { - return filterIsInstance().map { it.amount }.sumOrZero(currency) -} - -fun Cash.State.ownedBy(owner: AbstractParty) = copy(owner = owner) -fun Cash.State.issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) -fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) -fun Cash.State.withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) - -infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) -infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) -infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) +/** @suppress */ infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) +/** @suppress */ infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) +/** @suppress */ infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) +/** @suppress */ infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) // Unit testing helpers. These could go in a separate file but it's hardly worth it for just a few functions. diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt similarity index 75% rename from finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt index f9f0a13523..2ce6cc0855 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/CommodityContract.kt @@ -1,16 +1,19 @@ -package net.corda.contracts.asset +package net.corda.finance.contracts.asset -import net.corda.contracts.Commodity import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.newSecureRandom import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.Commodity +import net.corda.finance.utils.sumCommodities +import net.corda.finance.utils.sumCommoditiesOrNull +import net.corda.finance.utils.sumCommoditiesOrZero +import java.security.PublicKey import java.util.* + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Commodity @@ -31,18 +34,6 @@ val COMMODITY_PROGRAM_ID = CommodityContract() */ // TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc. class CommodityContract : OnLedgerAsset() { - /** - * TODO: - * 1) hash should be of the contents, not the URI - * 2) allow the content to be specified at time of instance creation? - * - * Motivation: it's the difference between a state object referencing a programRef, which references a - * legalContractReference and a state object which directly references both. The latter allows the legal wording - * to evolve without requiring code changes. But creates a risk that users create objects governed by a program - * that is inconsistent with the legal contract - */ - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html") - /** A state representing a commodity claim against some party */ data class State( override val amount: Amount>, @@ -54,10 +45,10 @@ class CommodityContract : OnLedgerAsset = Collections.singleton(owner.owningKey) override val participants = listOf(owner) - override fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset + override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) override fun toString() = "Commodity($amount at ${amount.token.issuer} owned by $owner)" @@ -67,27 +58,26 @@ class CommodityContract : OnLedgerAsset? = null) : MoveCommand /** - * Allows new commodity states to be issued into existence: the nonce ("number used once") ensures the transaction - * has a unique ID even when there are no inputs. + * Allows new commodity states to be issued into existence. */ - data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands + class Issue : TypeOnlyCommandData() /** * A command stating that money has been withdrawn from the shared ledger and is now accounted for * in some other way. */ - data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit + data class Exit(val amount: Amount>) : CommandData } override fun verify(tx: LedgerTransaction) { @@ -149,7 +139,6 @@ class CommodityContract : OnLedgerAsset() requireThat { - "the issue command has a nonce" using (issueCommand.value.nonce != 0L) "output deposits are owned by a command signer" using (issuer.party in issueCommand.signingParties) "output values sum to more than the inputs" using (outputAmount > inputAmount) "there is only a single issue command" using (commodityCommands.count() == 1) @@ -169,25 +158,12 @@ class CommodityContract : OnLedgerAsset>, owner: AbstractParty, notary: Party) - = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) + = generateIssue(tx, TransactionState(State(amount, owner), notary), Commands.Issue()) override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) - override fun generateIssueCommand() = Commands.Issue() override fun generateMoveCommand() = Commands.Move() } - -/** - * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash - * states cannot be added together (i.e. are different currencies). - */ -fun Iterable.sumCommodities() = filterIsInstance().map { it.amount }.sumOrThrow() - -/** Sums the cash states in the list, returning null if there are none. */ -@Suppress("unused") fun Iterable.sumCommoditiesOrNull() = filterIsInstance().map { it.amount }.sumOrNull() - -/** Sums the cash states in the list, returning zero of the given currency if there are none. */ -fun Iterable.sumCommoditiesOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero(currency) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt similarity index 89% rename from finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt index 2a37bda579..e41b8c6389 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/Obligation.kt @@ -1,23 +1,26 @@ -package net.corda.contracts.asset +package net.corda.finance.contracts.asset -import com.google.common.annotations.VisibleForTesting -import net.corda.contracts.NetCommand -import net.corda.contracts.NetType -import net.corda.contracts.NettableState -import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL +import net.corda.finance.contracts.NetCommand +import net.corda.finance.contracts.NetType +import net.corda.finance.contracts.NettableState +import net.corda.finance.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.crypto.random63BitValue import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.internal.Emoji +import net.corda.core.internal.VisibleForTesting import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.seconds +import net.corda.finance.utils.sumFungibleOrNull +import net.corda.finance.utils.sumObligations +import net.corda.finance.utils.sumObligationsOrNull +import net.corda.finance.utils.sumObligationsOrZero import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.PublicKey @@ -71,19 +74,6 @@ val OBLIGATION_PROGRAM_ID = Obligation() * @param P the product the obligation is for payment of. */ class Obligation

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

: Contract { override val participants: List = listOf(obligor, beneficiary) override val owner: AbstractParty = beneficiary - override fun move(newAmount: Amount>>, newOwner: AbstractParty): State

+ override fun withNewOwnerAndAmount(newAmount: Amount>>, newOwner: AbstractParty): State

= copy(quantity = newAmount.quantity, beneficiary = newOwner) override fun toString() = when (lifecycle) { @@ -187,40 +177,39 @@ class Obligation

: Contract { // Just for grouping @CordaSerializable - interface Commands : FungibleAsset.Commands { + interface Commands : CommandData { /** * Net two or more obligation states together in a close-out netting style. Limited to bilateral netting * as only the beneficiary (not the obligor) needs to sign. */ - data class Net(override val type: NetType) : NetCommand, Commands + data class Net(override val type: NetType) : NetCommand /** * A command stating that a debt has been moved, optionally to fulfil another contract. * - * @param contractHash the contract this move is for the attention of. Only that contract's verify function + * @param contract the contract this move is for the attention of. Only that contract's verify function * should take the moved states into account when considering whether it is valid. Typically this will be * null. */ - data class Move(override val contractHash: SecureHash? = null) : Commands, FungibleAsset.Commands.Move + data class Move(override val contract: Class? = null) : MoveCommand - /** - * Allows new obligation states to be issued into existence: the nonce ("number used once") ensures the - * transaction has a unique ID even when there are no inputs. + /** + * Allows new obligation states to be issued into existence. */ - data class Issue(override val nonce: Long = random63BitValue()) : FungibleAsset.Commands.Issue, Commands + class Issue : TypeOnlyCommandData() /** * A command stating that the obligor is settling some or all of the amount owed by transferring a suitable * state object to the beneficiary. If this reduces the balance to zero, the state object is destroyed. * @see [MoveCommand]. */ - data class Settle

(val amount: Amount>>) : Commands + data class Settle

(val amount: Amount>>) : CommandData /** * A command stating that the beneficiary is moving the contract into the defaulted state as it has not been settled * by the due date, or resetting a defaulted contract back to the issued state. */ - data class SetLifecycle(val lifecycle: Lifecycle) : Commands { + data class SetLifecycle(val lifecycle: Lifecycle) : CommandData { val inverse: Lifecycle get() = when (lifecycle) { Lifecycle.NORMAL -> Lifecycle.DEFAULTED @@ -232,7 +221,7 @@ class Obligation

: Contract { * A command stating that the debt is being released by the beneficiary. Normally would indicate * either settlement outside of the ledger, or that the obligor is unable to pay. */ - data class Exit

(override val amount: Amount>>) : Commands, FungibleAsset.Commands.Exit> + data class Exit

(val amount: Amount>>) : CommandData } override fun verify(tx: LedgerTransaction) { @@ -313,7 +302,6 @@ class Obligation

: Contract { val outputAmount = outputs.sumObligations

() val issueCommands = tx.commands.select() requireThat { - "the issue command has a nonce" using (issueCommand.value.nonce != 0L) "output states are issued by a command signer" using (issuer.party in issueCommand.signingParties) "output values sum to more than the inputs" using (outputAmount > inputAmount) "there is only a single issue command" using (issueCommands.count() == 1) @@ -349,15 +337,15 @@ class Obligation

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

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

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

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

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

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

::net) if (out.quantity > 0L) tx.addOutputState(out) @@ -517,7 +507,7 @@ class Obligation

: Contract { fun generateExit(tx: TransactionBuilder, amountIssued: Amount>>, assetStates: List>>): Set = OnLedgerAsset.generateExit(tx, amountIssued, assetStates, - deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) }, + deriveState = { state, amount, owner -> state.copy(data = state.data.withNewOwnerAndAmount(amount, owner)) }, generateMoveCommand = { -> Commands.Move() }, generateExitCommand = { amount -> Commands.Exit(amount) } ) @@ -537,11 +527,12 @@ class Obligation

: Contract { */ fun generateCashIssue(tx: TransactionBuilder, obligor: AbstractParty, + acceptableContract: SecureHash, amount: Amount>, dueBefore: Instant, beneficiary: AbstractParty, notary: Party) { - val issuanceDef = Terms(NonEmptySet.of(Cash().legalContractReference), NonEmptySet.of(amount.token), dueBefore) + val issuanceDef = Terms(NonEmptySet.of(acceptableContract), NonEmptySet.of(amount.token), dueBefore) OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, amount.quantity, beneficiary), notary), Commands.Issue()) } @@ -567,10 +558,14 @@ class Obligation

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

) { + vararg inputs: StateAndRef>) { + val states = inputs.map { it.state.data } requireThat { "all states are in the normal lifecycle state " using (states.all { it.lifecycle == Lifecycle.NORMAL }) } + + tx.withItems(*inputs) + val groups = states.groupBy { it.multilateralNetState } val partyLookup = HashMap() val signers = states.map { it.beneficiary }.union(states.map { it.obligor }).toSet() @@ -675,13 +670,13 @@ class Obligation

: Contract { val assetState = ref.state.data val amount = Amount(assetState.amount.quantity, assetState.amount.token.product) if (obligationRemaining >= amount) { - tx.addOutputState(assetState.move(assetState.amount, obligationOwner), notary) + tx.addOutputState(assetState.withNewOwnerAndAmount(assetState.amount, obligationOwner), notary) obligationRemaining -= amount } else { val change = Amount(obligationRemaining.quantity, assetState.amount.token) // Split the state in two, sending the change back to the previous beneficiary - tx.addOutputState(assetState.move(change, obligationOwner), notary) - tx.addOutputState(assetState.move(assetState.amount - change, assetState.owner), notary) + tx.addOutputState(assetState.withNewOwnerAndAmount(change, obligationOwner), notary) + tx.addOutputState(assetState.withNewOwnerAndAmount(assetState.amount - change, assetState.owner), notary) obligationRemaining -= Amount(0L, obligationRemaining.token) } assetSigners.add(assetState.owner) @@ -786,18 +781,6 @@ fun

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

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

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

Iterable.sumObligationsOrZero(issuanceDef: Issued>): Amount>> - = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef) - infix fun Obligation.State.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore)) infix fun Obligation.State.between(parties: Pair) = copy(obligor = parties.first, beneficiary = parties.second) infix fun Obligation.State.`owned by`(owner: AbstractParty) = copy(beneficiary = owner) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt similarity index 67% rename from finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt index c40649b789..7205ce5366 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/OnLedgerAsset.kt @@ -1,6 +1,8 @@ -package net.corda.contracts.asset +package net.corda.finance.contracts.asset import net.corda.core.contracts.* +import net.corda.core.contracts.Amount.Companion.sumOrThrow +import net.corda.core.contracts.Amount.Companion.zero import net.corda.core.identity.AbstractParty import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.loggerFor @@ -13,6 +15,9 @@ import java.util.* // Generic contract for assets on a ledger // +/** A simple holder for a (possibly anonymous) [AbstractParty] and a quantity of tokens */ +data class PartyAndAmount(val party: AbstractParty, val amount: Amount) + /** * An asset transaction may split and merge assets represented by a set of (issuer, depositRef) pairs, across multiple * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour (a blend of @@ -55,6 +60,38 @@ abstract class OnLedgerAsset> : C acceptableStates: List>, deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, generateMoveCommand: () -> CommandData): Pair> { + return generateSpend(tx, listOf(PartyAndAmount(to, amount)), acceptableStates, deriveState, generateMoveCommand) + } + + /** + * Adds to the given transaction states that move amounts of a fungible asset to the given parties, using only + * the provided acceptable input states to find a solution (not all of them may be used in the end). A change + * output will be generated if the state amounts don't exactly fit. + * + * The fungible assets must all be of the same type and the amounts must be summable i.e. amounts of the same + * token. + * + * @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed + * to move the cash will be added on top. + * @param amount How much currency to send. + * @param to a key of the recipient. + * @param acceptableStates a list of acceptable input states to use. + * @param deriveState a function to derive an output state based on an input state, amount for the output + * and public key to pay to. + * @param T A type representing a token + * @param S A fungible asset state type + * @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign + * the resulting transaction for it to be valid. + * @throws InsufficientBalanceException when a cash spending transaction fails because + * there is insufficient quantity for a given currency (and optionally set of Issuer Parties). + */ + @Throws(InsufficientBalanceException::class) + @JvmStatic + fun , T: Any> generateSpend(tx: TransactionBuilder, + payments: List>, + acceptableStates: List>, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, + generateMoveCommand: () -> CommandData): Pair> { // Discussion // // This code is analogous to the Wallet.send() set of methods in bitcoinj, and has the same general outline. @@ -79,43 +116,69 @@ abstract class OnLedgerAsset> : C // different notaries, or at least group states by notary and take the set with the // highest total value. - // notary may be associated with locked state only + // TODO: Check that re-running this on the same transaction multiple times does the right thing. + + // The notary may be associated with a locked state only. tx.notary = acceptableStates.firstOrNull()?.state?.notary - val (gathered, gatheredAmount) = gatherCoins(acceptableStates, amount) - - val takeChangeFrom = gathered.firstOrNull() - val change = if (takeChangeFrom != null && gatheredAmount > amount) { - Amount(gatheredAmount.quantity - amount.quantity, takeChangeFrom.state.data.amount.token) - } else { - null - } + // Calculate the total amount we're sending (they must be all of a compatible token). + val totalSendAmount = payments.map { it.amount }.sumOrThrow() + // Select a subset of the available states we were given that sums up to >= totalSendAmount. + val (gathered, gatheredAmount) = gatherCoins(acceptableStates, totalSendAmount) + check(gatheredAmount >= totalSendAmount) val keysUsed = gathered.map { it.state.data.owner.owningKey } - val states = gathered.groupBy { it.state.data.amount.token.issuer }.map { - val coins = it.value - val totalAmount = coins.map { it.state.data.amount }.sumOrThrow() - deriveState(coins.first().state, totalAmount, to) - }.sortedBy { it.data.amount.quantity } - - val outputs = if (change != null) { - // Just copy a key across as the change key. In real life of course, this works but leaks private data. - // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow - // value flows through the transaction graph. - val existingOwner = gathered.first().state.data.owner - // Add a change output and adjust the last output downwards. - states.subList(0, states.lastIndex) + - states.last().let { - val spent = it.data.amount.withoutIssuer() - change.withoutIssuer() - deriveState(it, Amount(spent.quantity, it.data.amount.token), it.data.owner) - } + - states.last().let { - deriveState(it, Amount(change.quantity, it.data.amount.token), existingOwner) + // Now calculate the output states. This is complicated by the fact that a single payment may require + // multiple output states, due to the need to keep states separated by issuer. We start by figuring out + // how much we've gathered for each issuer: this map will keep track of how much we've used from each + // as we work our way through the payments. + val statesGroupedByIssuer = gathered.groupBy { it.state.data.amount.token } + val remainingFromEachIssuer= statesGroupedByIssuer + .mapValues { + it.value.map { + it.state.data.amount + }.sumOrThrow() + }.toList().toMutableList() + val outputStates = mutableListOf>() + for ((party, paymentAmount) in payments) { + var remainingToPay= paymentAmount.quantity + while (remainingToPay > 0) { + val (token, remainingFromCurrentIssuer) = remainingFromEachIssuer.last() + val templateState = statesGroupedByIssuer[token]!!.first().state + val delta = remainingFromCurrentIssuer.quantity - remainingToPay + when { + delta > 0 -> { + // The states from the current issuer more than covers this payment. + outputStates += deriveState(templateState, Amount(remainingToPay, token), party) + remainingFromEachIssuer[0] = Pair(token, Amount(delta, token)) + remainingToPay = 0 } - } else states + delta == 0L -> { + // The states from the current issuer exactly covers this payment. + outputStates += deriveState(templateState, Amount(remainingToPay, token), party) + remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex) + remainingToPay = 0 + } + delta < 0 -> { + // The states from the current issuer don't cover this payment, so we'll have to use >1 output + // state to cover this payment. + outputStates += deriveState(templateState, remainingFromCurrentIssuer, party) + remainingFromEachIssuer.removeAt(remainingFromEachIssuer.lastIndex) + remainingToPay -= remainingFromCurrentIssuer.quantity + } + } + } + } + + // Whatever values we have left over for each issuer must become change outputs. + val myself = gathered.first().state.data.owner + for ((token, amount) in remainingFromEachIssuer) { + val templateState = statesGroupedByIssuer[token]!!.first().state + outputStates += deriveState(templateState, amount, myself) + } for (state in gathered) tx.addInputState(state) - for (state in outputs) tx.addOutputState(state) + for (state in outputStates) tx.addOutputState(state) // What if we already have a move command with the right keys? Filter it out here or in platform code? tx.addCommand(generateMoveCommand(), keysUsed) @@ -243,9 +306,8 @@ abstract class OnLedgerAsset> : C ) } - abstract fun generateExitCommand(amount: Amount>): FungibleAsset.Commands.Exit - abstract fun generateIssueCommand(): FungibleAsset.Commands.Issue - abstract fun generateMoveCommand(): FungibleAsset.Commands.Move + abstract fun generateExitCommand(amount: Amount>): CommandData + abstract fun generateMoveCommand(): MoveCommand /** * Derive a new transaction state based on the given example, with amount and owner modified. This allows concrete diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt new file mode 100644 index 0000000000..0ef6053ec2 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -0,0 +1,151 @@ +package net.corda.finance.contracts.asset.cash.selection + +import co.paralleluniverse.fibers.Suspendable +import co.paralleluniverse.strands.Strand +import net.corda.core.contracts.Amount +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.toBase58String +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.StatesNotAvailableException +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.utilities.* +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.CashSelection +import java.sql.DatabaseMetaData +import java.sql.SQLException +import java.util.* +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +class CashSelectionH2Impl : CashSelection { + + companion object { + val JDBC_DRIVER_NAME = "H2 JDBC Driver" + val log = loggerFor() + } + + override fun isCompatible(metaData: DatabaseMetaData): Boolean { + return metaData.driverName == JDBC_DRIVER_NAME + } + + // coin selection retry loop counter, sleep (msecs) and lock for selecting states + private val MAX_RETRIES = 5 + private val RETRY_SLEEP = 100 + private val spendLock: ReentrantLock = ReentrantLock() + + /** + * An optimised query to gather Cash states that are available and retry if they are temporarily unavailable. + * @param services The service hub to allow access to the database session + * @param amount The amount of currency desired (ignoring issues, but specifying the currency) + * @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer, + * otherwise the set of eligible states wil be filtered to only include those from these issuers. + * @param notary If null the notary source is ignored, if specified then only states marked + * with this notary are included. + * @param lockId The FlowLogic.runId.uuid of the flow, which is used to soft reserve the states. + * Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes. + * @param withIssuerRefs If not empty the specific set of issuer references to match against. + * @return The matching states that were found. If sufficient funds were found these will be locked, + * otherwise what is available is returned unlocked for informational purposes. + */ + @Suspendable + override fun unconsumedCashStatesForSpending(services: ServiceHub, + amount: Amount, + onlyFromIssuerParties: Set, + notary: Party?, + lockId: UUID, + withIssuerRefs: Set): List> { + + val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1) + val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1) + + val stateAndRefs = mutableListOf>() + + // We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins: + // 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the + // running total of such an accumulator + // 2) H2 uses session variables to perform this accumulator function: + // http://www.h2database.com/html/functions.html#set + // 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries) + + for (retryCount in 1..MAX_RETRIES) { + + spendLock.withLock { + val statement = services.jdbcSession().createStatement() + try { + statement.execute("CALL SET(@t, 0);") + + // we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null) + // the softLockReserve update will detect whether we try to lock states locked by others + val selectJoin = """ + SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id + FROM vault_states AS vs, contract_cash_states AS ccs + WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index + AND vs.state_status = 0 + AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity} + AND (vs.lock_id = '$lockId' OR vs.lock_id is null) + """ + + (if (notary != null) + " AND vs.notary_name = '${notary.name}'" else "") + + (if (onlyFromIssuerParties.isNotEmpty()) + " AND ccs.issuer_key IN ($issuerKeysStr)" else "") + + (if (withIssuerRefs.isNotEmpty()) + " AND ccs.issuer_ref IN ($issuerRefsStr)" else "") + + // Retrieve spendable state refs + val rs = statement.executeQuery(selectJoin) + stateAndRefs.clear() + log.debug(selectJoin) + var totalPennies = 0L + while (rs.next()) { + val txHash = SecureHash.parse(rs.getString(1)) + val index = rs.getInt(2) + val stateRef = StateRef(txHash, index) + val state = rs.getBytes(3).deserialize>(context = SerializationDefaults.STORAGE_CONTEXT) + val pennies = rs.getLong(4) + totalPennies = rs.getLong(5) + val rowLockId = rs.getString(6) + stateAndRefs.add(StateAndRef(state, stateRef)) + log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" } + } + + if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) { + // we should have a minimum number of states to satisfy our selection `amount` criteria + log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs") + + // With the current single threaded state machine available states are guaranteed to lock. + // TODO However, we will have to revisit these methods in the future multi-threaded. + services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet()) + return stateAndRefs + } + log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}") + // retry as more states may become available + } catch (e: SQLException) { + log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId] + $e. + """) + } catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine + stateAndRefs.clear() + log.warn(e.message) + // retry only if there are locked states that may become available again (or consumed with change) + } finally { + statement.close() + } + } + + log.warn("Coin selection failed on attempt $retryCount") + // TODO: revisit the back off strategy for contended spending. + if (retryCount != MAX_RETRIES) { + Strand.sleep(RETRY_SLEEP * retryCount.toLong()) + } + } + + log.warn("Insufficient spendable states identified for $amount") + return stateAndRefs + } +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt new file mode 100644 index 0000000000..0651bcdf70 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionMySQLImpl.kt @@ -0,0 +1,32 @@ +package net.corda.finance.contracts.asset.cash.selection + +import net.corda.core.contracts.Amount +import net.corda.core.contracts.StateAndRef +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.CashSelection +import java.sql.DatabaseMetaData +import java.util.* + +class CashSelectionMySQLImpl : CashSelection { + + companion object { + val JDBC_DRIVER_NAME = "MySQL JDBC Driver" + } + + override fun isCompatible(metadata: DatabaseMetaData): Boolean { + return metadata.driverName == JDBC_DRIVER_NAME + } + + override fun unconsumedCashStatesForSpending(services: ServiceHub, + amount: Amount, + onlyFromIssuerParties: Set, + notary: Party?, + lockId: UUID, + withIssuerRefs: Set): List> { + TODO("MySQL cash selection not implemented") + } + } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/contracts/math/Interpolators.kt b/finance/src/main/kotlin/net/corda/finance/contracts/math/Interpolators.kt similarity index 99% rename from finance/src/main/kotlin/net/corda/contracts/math/Interpolators.kt rename to finance/src/main/kotlin/net/corda/finance/contracts/math/Interpolators.kt index 62fc40a4c6..294ff5e326 100644 --- a/finance/src/main/kotlin/net/corda/contracts/math/Interpolators.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/math/Interpolators.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.math +package net.corda.finance.contracts.math import java.util.* diff --git a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/AbstractCashFlow.kt similarity index 92% rename from finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt rename to finance/src/main/kotlin/net/corda/finance/flows/AbstractCashFlow.kt index fd809ab7e5..f86a3c52d0 100644 --- a/finance/src/main/kotlin/net/corda/flows/AbstractCashFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/AbstractCashFlow.kt @@ -1,6 +1,7 @@ -package net.corda.flows +package net.corda.finance.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount import net.corda.core.flows.FinalityFlow import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic @@ -10,6 +11,7 @@ import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker +import java.util.* /** * Initiates a flow that produces an Issue/Move or Exit Cash transaction. @@ -44,6 +46,8 @@ abstract class AbstractCashFlow(override val progressTracker: ProgressTra */ @CordaSerializable data class Result(val stx: SignedTransaction, val recipient: AbstractParty?) + + abstract class AbstractRequest(val amount: Amount) } class CashException(message: String, cause: Throwable) : FlowException(message, cause) \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt similarity index 75% rename from finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt rename to finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt index abf0b511c8..c2a5386262 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashExitFlow.kt @@ -1,19 +1,21 @@ -package net.corda.flows +package net.corda.finance.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.InsufficientBalanceException -import net.corda.core.contracts.issuedBy import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.ProgressTracker +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.CashSelection +import net.corda.finance.issuedBy import java.util.* /** @@ -24,8 +26,9 @@ import java.util.* * issuer. */ @StartableByRPC -class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { +class CashExitFlow(val amount: Amount, val issuerRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, issueRef: OpaqueBytes) : this(amount, issueRef, tracker()) + constructor(request: ExitRequest) : this(request.amount, request.issueRef, tracker()) companion object { fun tracker() = ProgressTracker(GENERATING_TX, SIGNING_TX, FINALISING_TX) @@ -39,9 +42,9 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog @Throws(CashException::class) override fun call(): AbstractCashFlow.Result { progressTracker.currentStep = GENERATING_TX - val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?) - val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) - val exitStates = Cash.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) + val builder = TransactionBuilder(notary = null as Party?) + val issuer = serviceHub.myInfo.legalIdentity.ref(issuerRef) + val exitStates = CashSelection.getInstance({serviceHub.jdbcSession().metaData}).unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) val signers = try { Cash().generateExit( builder, @@ -71,4 +74,7 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog finaliseTx(participants, tx, "Unable to notarise exit") return Result(tx, null) } + + @CordaSerializable + class ExitRequest(amount: Amount, val issueRef: OpaqueBytes) : AbstractRequest(amount) } diff --git a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashFlowCommand.kt similarity index 79% rename from finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt rename to finance/src/main/kotlin/net/corda/finance/flows/CashFlowCommand.kt index 446cca5b4d..ae689ce66b 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashFlowCommand.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashFlowCommand.kt @@ -1,4 +1,4 @@ -package net.corda.flows +package net.corda.finance.flows import net.corda.core.contracts.Amount import net.corda.core.identity.Party @@ -6,11 +6,13 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow import java.util.* /** * A command to initiate the cash flow with. */ +@Deprecated("Please use the flows directly, these will be removed in a later release") sealed class CashFlowCommand { abstract fun startFlow(proxy: CordaRPCOps): FlowHandle @@ -22,7 +24,10 @@ sealed class CashFlowCommand { val recipient: Party, val notary: Party, val anonymous: Boolean) : CashFlowCommand() { - override fun startFlow(proxy: CordaRPCOps) = proxy.startFlow(::CashIssueFlow, amount, issueRef, recipient, notary, anonymous) + override fun startFlow(proxy: CordaRPCOps): FlowHandle { + proxy.startFlow(::CashIssueFlow, amount, issueRef, notary).returnValue.getOrThrow() + return proxy.startFlow(::CashPaymentFlow, amount, recipient, anonymous) + } } /** diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt new file mode 100644 index 0000000000..48f0f43c4a --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueAndPaymentFlow.kt @@ -0,0 +1,49 @@ +package net.corda.finance.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import java.util.* + +/** + * Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction). + * + * We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk + * checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash + * and immediately transfer it, so impact of this limitation is considered minimal. + * + * @param amount the amount of currency to issue. + * @param issuerBankPartyRef a reference to put on the issued currency. + * @param notary the notary to set on the output states. + */ +@StartableByRPC +class CashIssueAndPaymentFlow(val amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val anonymous: Boolean, + val notary: Party, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(amount: Amount, + issueRef: OpaqueBytes, + recipient: Party, + anonymous: Boolean, + notary: Party) : this(amount, issueRef, recipient, anonymous, notary, tracker()) + constructor(request: IssueAndPaymentRequest) : this(request.amount, request.issueRef, request.recipient, request.anonymous, request.notary, tracker()) + + @Suspendable + override fun call(): Result { + subFlow(CashIssueFlow(amount, issueRef, notary)) + return subFlow(CashPaymentFlow(amount, recipient, anonymous)) + } + + @CordaSerializable + class IssueAndPaymentRequest(amount: Amount, + val issueRef: OpaqueBytes, + val recipient: Party, + val notary: Party, + val anonymous: Boolean) : AbstractRequest(amount) +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt new file mode 100644 index 0000000000..99ad4cffa5 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashIssueFlow.kt @@ -0,0 +1,54 @@ +package net.corda.finance.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Amount +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.ProgressTracker +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.issuedBy +import java.util.* + +/** + * Initiates a flow that self-issues cash (which should then be sent to recipient(s) using a payment transaction). + * + * We issue cash only to ourselves so that all KYC/AML checks on payments are enforced consistently, rather than risk + * checks for issuance and payments differing. Outside of test scenarios it would be extremely unusual to issue cash + * and immediately transfer it, so impact of this limitation is considered minimal. + * + * @param amount the amount of currency to issue. + * @param issuerBankPartyRef a reference to put on the issued currency. + * @param notary the notary to set on the output states. + */ +@StartableByRPC +class CashIssueFlow(val amount: Amount, + val issuerBankPartyRef: OpaqueBytes, + val notary: Party, + progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { + constructor(amount: Amount, + issuerBankPartyRef: OpaqueBytes, + notary: Party) : this(amount, issuerBankPartyRef, notary, tracker()) + constructor(request: IssueRequest) : this(request.amount, request.issueRef, request.notary, tracker()) + + @Suspendable + override fun call(): AbstractCashFlow.Result { + val issuerCert = serviceHub.myInfo.legalIdentityAndCert + + progressTracker.currentStep = GENERATING_TX + val builder = TransactionBuilder(notary) + val issuer = issuerCert.party.ref(issuerBankPartyRef) + val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), issuerCert.party, notary) + progressTracker.currentStep = SIGNING_TX + val tx = serviceHub.signInitialTransaction(builder, signers) + progressTracker.currentStep = FINALISING_TX + subFlow(FinalityFlow(tx)) + return Result(tx, issuerCert.party) + } + + @CordaSerializable + class IssueRequest(amount: Amount, val issueRef: OpaqueBytes, val notary: Party) : AbstractRequest(amount) +} diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt similarity index 81% rename from finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt rename to finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt index 1b3032c565..c69b2e98b5 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/CashPaymentFlow.kt @@ -1,15 +1,16 @@ -package net.corda.flows +package net.corda.finance.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.flows.StartableByRPC import net.corda.core.flows.TransactionKeyFlow import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker +import net.corda.finance.contracts.asset.Cash import java.util.* /** @@ -32,6 +33,7 @@ open class CashPaymentFlow( constructor(amount: Amount, recipient: Party) : this(amount, recipient, true, tracker()) /** A straightforward constructor that constructs spends using cash states of any issuer. */ constructor(amount: Amount, recipient: Party, anonymous: Boolean) : this(amount, recipient, anonymous, tracker()) + constructor(request: PaymentRequest) : this(request.amount, request.recipient, request.anonymous, tracker(), request.issuerConstraint) @Suspendable override fun call(): AbstractCashFlow.Result { @@ -41,9 +43,9 @@ open class CashPaymentFlow( } else { emptyMap() } - val anonymousRecipient = txIdentities.get(recipient) ?: recipient + val anonymousRecipient = txIdentities[recipient] ?: recipient progressTracker.currentStep = GENERATING_TX - val builder: TransactionBuilder = TransactionBuilder(null as Party?) + val builder = TransactionBuilder(null as Party?) // TODO: Have some way of restricting this to states the caller controls val (spendTX, keysForSigning) = try { Cash.generateSpend(serviceHub, @@ -62,4 +64,7 @@ open class CashPaymentFlow( finaliseTx(setOf(recipient), tx, "Unable to notarise spend") return Result(tx, anonymousRecipient) } + + @CordaSerializable + class PaymentRequest(amount: Amount, val recipient: Party, val anonymous: Boolean, val issuerConstraint: Set = emptySet()) : AbstractRequest(amount) } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt similarity index 99% rename from finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt rename to finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt index 358de91328..b408708da4 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyDealFlow.kt @@ -1,7 +1,6 @@ -package net.corda.flows +package net.corda.finance.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.DealState import net.corda.core.contracts.requireThat import net.corda.core.crypto.SecureHash import net.corda.core.crypto.TransactionSignature @@ -20,6 +19,7 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.seconds import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap +import net.corda.finance.contracts.DealState import java.security.PublicKey /** diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt similarity index 98% rename from finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt rename to finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt index e63be156e3..215946ae9b 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/finance/flows/TwoPartyTradeFlow.kt @@ -1,8 +1,6 @@ -package net.corda.flows +package net.corda.finance.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.Amount import net.corda.core.contracts.OwnableState import net.corda.core.contracts.StateAndRef @@ -18,6 +16,8 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.seconds import net.corda.core.utilities.unwrap +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.utils.sumCashBy import java.security.PublicKey import java.util.* diff --git a/finance/src/main/kotlin/net/corda/finance/plugin/FinanceJSONSupport.kt b/finance/src/main/kotlin/net/corda/finance/plugin/FinanceJSONSupport.kt new file mode 100644 index 0000000000..3384878d9e --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/plugin/FinanceJSONSupport.kt @@ -0,0 +1,50 @@ +@file:JvmName("FinanceJSONSupport") + +package net.corda.finance.plugin + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.* +import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer +import com.fasterxml.jackson.databind.module.SimpleModule +import net.corda.finance.contracts.BusinessCalendar +import java.time.LocalDate + +fun registerFinanceJSONMappers(objectMapper: ObjectMapper): Unit { + val financeModule = SimpleModule("finance").apply { + addSerializer(BusinessCalendar::class.java, CalendarSerializer) + addDeserializer(BusinessCalendar::class.java, CalendarDeserializer) + } + objectMapper.registerModule(financeModule) +} + +data class BusinessCalendarWrapper(val holidayDates: List) { + fun toCalendar() = BusinessCalendar(holidayDates) +} + +object CalendarSerializer : JsonSerializer() { + 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() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar { + return try { + 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}") + } + } +} diff --git a/finance/src/main/kotlin/net/corda/finance/plugin/FinancePluginRegistry.kt b/finance/src/main/kotlin/net/corda/finance/plugin/FinancePluginRegistry.kt new file mode 100644 index 0000000000..31c0db0b05 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/plugin/FinancePluginRegistry.kt @@ -0,0 +1,11 @@ +package net.corda.finance.plugin + +import net.corda.core.node.CordaPluginRegistry +import net.corda.core.schemas.MappedSchema +import net.corda.finance.schemas.CashSchemaV1 + +class FinancePluginRegistry : CordaPluginRegistry() { + override val requiredSchemas: Set = setOf( + CashSchemaV1 + ) +} \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt b/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt similarity index 78% rename from finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt rename to finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt index 8e11e1f8c8..6ae8d9a9a0 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/finance/schemas/CashSchemaV1.kt @@ -1,9 +1,13 @@ -package net.corda.schemas +package net.corda.finance.schemas +import net.corda.core.identity.AbstractParty import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable -import javax.persistence.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Index +import javax.persistence.Table /** * An object used to fully qualify the [CashSchema] family name (i.e. independent of version). @@ -21,8 +25,9 @@ object CashSchemaV1 : MappedSchema(schemaFamily = CashSchema.javaClass, version indexes = arrayOf(Index(name = "ccy_code_idx", columnList = "ccy_code"), Index(name = "pennies_idx", columnList = "pennies"))) class PersistentCashState( - @Column(name = "owner_key") - var owner: String, + /** X500Name of owner party **/ + @Column(name = "owner_name") + var owner: AbstractParty, @Column(name = "pennies") var pennies: Long, diff --git a/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt b/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt similarity index 98% rename from finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt rename to finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt index 98d600ed09..b1d58a5c8c 100644 --- a/finance/src/main/kotlin/net/corda/schemas/CommercialPaperSchemaV1.kt +++ b/finance/src/main/kotlin/net/corda/finance/schemas/CommercialPaperSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.schemas +package net.corda.finance.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState diff --git a/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt new file mode 100644 index 0000000000..75d6c072f9 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/utils/StateSummingUtilities.kt @@ -0,0 +1,73 @@ +@file:JvmName("StateSumming") +package net.corda.finance.utils + +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Amount.Companion.sumOrNull +import net.corda.core.contracts.Amount.Companion.sumOrThrow +import net.corda.core.contracts.Amount.Companion.sumOrZero +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.Issued +import net.corda.core.identity.AbstractParty +import net.corda.finance.contracts.Commodity +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.CommodityContract +import net.corda.finance.contracts.asset.Obligation +import java.util.* + +/** A collection of utilities for summing states */ + +/** + * Sums the cash states in the list belonging to a single owner, throwing an exception + * if there are none, or if any of the cash states cannot be added together (i.e. are + * different currencies or issuers). + */ +fun Iterable.sumCashBy(owner: AbstractParty): Amount> = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() + +/** + * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash + * states cannot be added together (i.e. are different currencies or issuers). + */ +fun Iterable.sumCash(): Amount> = filterIsInstance().map { it.amount }.sumOrThrow() + +/** Sums the cash states in the list, returning null if there are none. */ +fun Iterable.sumCashOrNull(): Amount>? = filterIsInstance().map { it.amount }.sumOrNull() + +/** Sums the cash states in the list, returning zero of the given currency+issuer if there are none. */ +fun Iterable.sumCashOrZero(currency: Issued): Amount> { + return filterIsInstance().map { it.amount }.sumOrZero(currency) +} + +/** Sums the asset states in the list, returning null if there are none. */ +fun Iterable.sumFungibleOrNull() = filterIsInstance>().map { it.amount }.sumOrNull() + +/** Sums the asset states in the list, returning zero of the given token if there are none. */ +fun Iterable.sumFungibleOrZero(token: Issued) = filterIsInstance>().map { it.amount }.sumOrZero(token) + +/** + * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash + * states cannot be added together (i.e. are different currencies). + */ +fun Iterable.sumCommodities() = filterIsInstance().map { it.amount }.sumOrThrow() + +/** Sums the cash states in the list, returning null if there are none. */ +@Suppress("unused") +fun Iterable.sumCommoditiesOrNull() = filterIsInstance().map { it.amount }.sumOrNull() + +/** Sums the cash states in the list, returning zero of the given currency if there are none. */ +fun Iterable.sumCommoditiesOrZero(currency: Issued) = filterIsInstance().map { it.amount }.sumOrZero(currency) + +/** + * Sums the obligation states in the list, throwing an exception if there are none. All state objects in the + * list are presumed to be nettable. + */ +fun

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

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

Iterable.sumObligationsOrZero(issuanceDef: Issued>): Amount>> + = filterIsInstance>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef) diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt deleted file mode 100644 index 99304f122c..0000000000 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ /dev/null @@ -1,61 +0,0 @@ -package net.corda.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.Amount -import net.corda.core.contracts.issuedBy -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.StartableByRPC -import net.corda.core.flows.TransactionKeyFlow -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.ProgressTracker -import java.util.* - -/** - * Initiates a flow that produces cash issuance transaction. - * - * @param amount the amount of currency to issue. - * @param issueRef a reference to put on the issued currency. - * @param recipient the party who should own the currency after it is issued. - * @param notary the notary to set on the output states. - */ -@StartableByRPC -class CashIssueFlow(val amount: Amount, - val issueRef: OpaqueBytes, - val recipient: Party, - val notary: Party, - val anonymous: Boolean, - progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { - constructor(amount: Amount, - issueRef: OpaqueBytes, - recipient: Party, - notary: Party) : this(amount, issueRef, recipient, notary, true, tracker()) - constructor(amount: Amount, - issueRef: OpaqueBytes, - recipient: Party, - notary: Party, - anonymous: Boolean) : this(amount, issueRef, recipient, notary, anonymous, tracker()) - - @Suspendable - override fun call(): AbstractCashFlow.Result { - progressTracker.currentStep = GENERATING_ID - val txIdentities = if (anonymous) { - subFlow(TransactionKeyFlow(recipient)) - } else { - emptyMap() - } - val anonymousRecipient = txIdentities[recipient] ?: recipient - progressTracker.currentStep = GENERATING_TX - val builder: TransactionBuilder = TransactionBuilder(notary) - val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) - val signers = Cash().generateIssue(builder, amount.issuedBy(issuer), anonymousRecipient, notary) - progressTracker.currentStep = SIGNING_TX - val tx = serviceHub.signInitialTransaction(builder, signers) - progressTracker.currentStep = FINALISING_TX - subFlow(FinalityFlow(tx)) - return Result(tx, anonymousRecipient) - } -} diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt deleted file mode 100644 index ccd6001fdf..0000000000 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ /dev/null @@ -1,117 +0,0 @@ -package net.corda.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.* -import net.corda.core.flows.* -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.unwrap -import java.util.* - -/** - * This flow enables a client to request issuance of some [FungibleAsset] from a - * server acting as an issuer (see [Issued]) of FungibleAssets. - * - * It is not intended for production usage, but rather for experimentation and testing purposes where it may be - * useful for creation of fake assets. - */ -object IssuerFlow { - @CordaSerializable - data class IssuanceRequestState(val amount: Amount, - val issueToParty: Party, - val issuerPartyRef: OpaqueBytes, - val notaryParty: Party, - val anonymous: Boolean) - - /** - * IssuanceRequester should be used by a client to ask a remote node to issue some [FungibleAsset] with the given details. - * Returns the transaction created by the Issuer to move the cash to the Requester. - * - * @param anonymous true if the issued asset should be sent to a new confidential identity, false to send it to the - * well known identity (generally this is only used in testing). - */ - @InitiatingFlow - @StartableByRPC - class IssuanceRequester(val amount: Amount, - val issueToParty: Party, - val issueToPartyRef: OpaqueBytes, - val issuerBankParty: Party, - val notaryParty: Party, - val anonymous: Boolean) : FlowLogic() { - @Suspendable - @Throws(CashException::class) - override fun call(): AbstractCashFlow.Result { - val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef, notaryParty, anonymous) - return sendAndReceive(issuerBankParty, issueRequest).unwrap { res -> - val tx = res.stx.tx - val expectedAmount = Amount(amount.quantity, Issued(issuerBankParty.ref(issueToPartyRef), amount.token)) - val cashOutputs = tx.filterOutputs { state -> state.owner == res.recipient } - require(cashOutputs.size == 1) { "Require a single cash output paying ${res.recipient}, found ${tx.outputs}" } - require(cashOutputs.single().amount == expectedAmount) { "Require payment of $expectedAmount"} - res - } - } - } - - /** - * Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client. - * Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester. - */ - @InitiatedBy(IssuanceRequester::class) - class Issuer(val otherParty: Party) : FlowLogic() { - companion object { - object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request") - object ISSUING : ProgressTracker.Step("Self issuing asset") - object TRANSFERRING : ProgressTracker.Step("Transferring asset to issuance requester") - object SENDING_CONFIRM : ProgressTracker.Step("Confirming asset issuance to requester") - - fun tracker() = ProgressTracker(AWAITING_REQUEST, ISSUING, TRANSFERRING, SENDING_CONFIRM) - private val VALID_CURRENCIES = listOf(USD, GBP, EUR, CHF) - } - - override val progressTracker: ProgressTracker = tracker() - - @Suspendable - @Throws(CashException::class) - override fun call(): SignedTransaction { - progressTracker.currentStep = AWAITING_REQUEST - val issueRequest = receive(otherParty).unwrap { - // validate request inputs (for example, lets restrict the types of currency that can be issued) - if (it.amount.token !in VALID_CURRENCIES) throw FlowException("Currency must be one of $VALID_CURRENCIES") - it - } - // TODO: parse request to determine Asset to issue - val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef, issueRequest.notaryParty, issueRequest.anonymous) - progressTracker.currentStep = SENDING_CONFIRM - send(otherParty, txn) - return txn.stx - } - - @Suspendable - private fun issueCashTo(amount: Amount, - issueTo: Party, - issuerPartyRef: OpaqueBytes, - notaryParty: Party, - anonymous: Boolean): AbstractCashFlow.Result { - // invoke Cash subflow to issue Asset - progressTracker.currentStep = ISSUING - val issueRecipient = serviceHub.myInfo.legalIdentity - val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, issueRecipient, notaryParty, anonymous) - val issueTx = subFlow(issueCashFlow) - // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) - // short-circuit when issuing to self - if (issueTo == serviceHub.myInfo.legalIdentity) - return issueTx - // now invoke Cash subflow to Move issued assetType to issue requester - progressTracker.currentStep = TRANSFERRING - val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous) - val moveTx = subFlow(moveCashFlow) - // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) - return moveTx - } - } -} \ No newline at end of file diff --git a/finance/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/finance/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry new file mode 100644 index 0000000000..b73664a311 --- /dev/null +++ b/finance/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry @@ -0,0 +1 @@ +net.corda.finance.plugin.FinancePluginRegistry diff --git a/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection b/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection new file mode 100644 index 0000000000..9ac2461b3c --- /dev/null +++ b/finance/src/main/resources/META-INF/services/net.corda.finance.contracts.asset.CashSelection @@ -0,0 +1,2 @@ +net.corda.finance.contracts.asset.cash.selection.CashSelectionH2Impl +net.corda.finance.contracts.asset.cash.selection.CashSelectionMySQLImpl diff --git a/finance/src/main/resources/net/corda/contracts/LondonHolidayCalendar.txt b/finance/src/main/resources/net/corda/finance/contracts/LondonHolidayCalendar.txt similarity index 100% rename from finance/src/main/resources/net/corda/contracts/LondonHolidayCalendar.txt rename to finance/src/main/resources/net/corda/finance/contracts/LondonHolidayCalendar.txt diff --git a/finance/src/main/resources/net/corda/contracts/NewYorkHolidayCalendar.txt b/finance/src/main/resources/net/corda/finance/contracts/NewYorkHolidayCalendar.txt similarity index 100% rename from finance/src/main/resources/net/corda/contracts/NewYorkHolidayCalendar.txt rename to finance/src/main/resources/net/corda/finance/contracts/NewYorkHolidayCalendar.txt diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java similarity index 87% rename from finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java rename to finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java index ba479db499..e1f8d92f9c 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/finance/contracts/asset/CashTestsJava.java @@ -1,4 +1,4 @@ -package net.corda.contracts.asset; +package net.corda.finance.contracts.asset; import kotlin.Unit; import net.corda.core.contracts.PartyAndReference; @@ -6,8 +6,8 @@ import net.corda.core.identity.AnonymousParty; import net.corda.core.utilities.OpaqueBytes; import org.junit.Test; -import static net.corda.core.contracts.ContractsDSL.DOLLARS; -import static net.corda.core.contracts.ContractsDSL.issuedBy; +import static net.corda.finance.CurrencyUtils.DOLLARS; +import static net.corda.finance.CurrencyUtils.issuedBy; import static net.corda.testing.CoreTestUtils.*; /** @@ -34,7 +34,7 @@ public class CashTestsJava { tx.tweak(tw -> { tw.output(outState); // No command arguments - return tw.failsWith("required net.corda.contracts.asset.Cash.Commands.Move command"); + return tw.failsWith("required net.corda.finance.contracts.asset.Cash.Commands.Move command"); }); tx.tweak(tw -> { tw.output(outState); @@ -45,7 +45,7 @@ public class CashTestsJava { tw.output(outState); // issuedBy() can't be directly imported because it conflicts with other identically named functions // with different overloads (for some reason). - tw.output(CashKt.issuedBy(outState, getMINI_CORP())); + tw.output(outState.issuedBy(getMINI_CORP())); tw.command(getMEGA_CORP_PUBKEY(), new Cash.Commands.Move()); return tw.failsWith("at least one cash input"); }); diff --git a/finance/src/test/java/net/corda/flows/AbstractStateReplacementFlowTest.java b/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java similarity index 96% rename from finance/src/test/java/net/corda/flows/AbstractStateReplacementFlowTest.java rename to finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java index c278197812..3b78a34881 100644 --- a/finance/src/test/java/net/corda/flows/AbstractStateReplacementFlowTest.java +++ b/finance/src/test/java/net/corda/finance/flows/AbstractStateReplacementFlowTest.java @@ -1,4 +1,4 @@ -package net.corda.flows; +package net.corda.finance.flows; import net.corda.core.flows.AbstractStateReplacementFlow; import net.corda.core.identity.Party; diff --git a/finance/src/test/kotlin/net/corda/finance/CurrencyUtilsTest.kt b/finance/src/test/kotlin/net/corda/finance/CurrencyUtilsTest.kt new file mode 100644 index 0000000000..1b294286cf --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/CurrencyUtilsTest.kt @@ -0,0 +1,30 @@ +package net.corda.finance + +import net.corda.core.contracts.Amount +import org.junit.Test +import kotlin.test.assertEquals + +class CurrencyUtilsTest { + @Test + fun `basic currency`() { + val expected = 1000L + val amount = Amount(expected, GBP) + assertEquals(expected, amount.quantity) + } + + @Test + fun parseCurrency() { + assertEquals(Amount(1234L, GBP), Amount.parseCurrency("£12.34")) + assertEquals(Amount(1200L, GBP), Amount.parseCurrency("£12")) + assertEquals(Amount(1000L, USD), Amount.parseCurrency("$10")) + assertEquals(Amount(5000L, JPY), Amount.parseCurrency("¥5000")) + assertEquals(Amount(500000L, RUB), Amount.parseCurrency("₽5000")) + assertEquals(Amount(1500000000L, CHF), Amount.parseCurrency("15,000,000 CHF")) + } + + @Test + fun rendering() { + assertEquals("5000 JPY", Amount.parseCurrency("¥5000").toString()) + assertEquals("50.12 USD", Amount.parseCurrency("$50.12").toString()) + } +} \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt similarity index 98% rename from finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt rename to finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt index 4f45920180..7953afa693 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/CommercialPaperTests.kt @@ -1,6 +1,5 @@ -package net.corda.contracts +package net.corda.finance.contracts -import net.corda.contracts.asset.* import net.corda.core.contracts.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party @@ -10,6 +9,9 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.days import net.corda.core.utilities.seconds +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.* import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash import net.corda.testing.node.MockServices @@ -105,7 +107,7 @@ class CommercialPaperTestsGeneric { input("paper") input("alice's $900") output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } - output("alice's paper") { "paper".output() `owned by` ALICE } + output("alice's paper") { "paper".output().withOwner(ALICE) } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } this.verifies() diff --git a/finance/src/test/kotlin/net/corda/contracts/FinanceTypesTest.kt b/finance/src/test/kotlin/net/corda/finance/contracts/FinanceTypesTest.kt similarity index 99% rename from finance/src/test/kotlin/net/corda/contracts/FinanceTypesTest.kt rename to finance/src/test/kotlin/net/corda/finance/contracts/FinanceTypesTest.kt index 8e9618b035..3e5c608a63 100644 --- a/finance/src/test/kotlin/net/corda/contracts/FinanceTypesTest.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/FinanceTypesTest.kt @@ -1,4 +1,4 @@ -package net.corda.contracts +package net.corda.finance.contracts import org.junit.Test import java.time.LocalDate diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt similarity index 91% rename from finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt rename to finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt index c2ab2d3d75..2f4227472c 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/CashTests.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.asset +package net.corda.finance.contracts.asset import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash @@ -11,6 +11,11 @@ import net.corda.core.node.services.queryBy import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.OpaqueBytes +import net.corda.finance.* +import net.corda.finance.utils.sumCash +import net.corda.finance.utils.sumCashBy +import net.corda.finance.utils.sumCashOrNull +import net.corda.finance.utils.sumCashOrZero import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.CordaPersistence import net.corda.testing.* @@ -54,6 +59,7 @@ class CashTests : TestDependencyInjectionBase() { database = databaseAndServices.first miniCorpServices = databaseAndServices.second + // Create some cash. Any attempt to spend >$500 will require multiple issuers to be involved. database.transaction { miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, ownedBy = OUR_IDENTITY_1, issuedBy = MEGA_CORP.ref(1), issuerServices = megaCorpServices) @@ -63,7 +69,8 @@ class CashTests : TestDependencyInjectionBase() { ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices) miniCorpServices.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1, ownedBy = OUR_IDENTITY_1, issuedBy = MINI_CORP.ref(1), issuerServices = miniCorpServices) - + } + database.transaction { vaultStatesUnconsumed = miniCorpServices.vaultQueryService.queryBy().states } resetTestSerialization() @@ -87,7 +94,7 @@ class CashTests : TestDependencyInjectionBase() { tweak { output { outState } // No command arguments - this `fails with` "required net.corda.contracts.asset.Cash.Commands.Move command" + this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command" } tweak { output { outState } @@ -137,10 +144,6 @@ class CashTests : TestDependencyInjectionBase() { owner = AnonymousParty(ALICE_PUBKEY) ) } - tweak { - command(MINI_CORP_PUBKEY) { Cash.Commands.Issue(0) } - this `fails with` "has a nonce" - } command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() } this.verifies() } @@ -351,7 +354,7 @@ class CashTests : TestDependencyInjectionBase() { tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) } - this `fails with` "required net.corda.contracts.asset.Cash.Commands.Move command" + this `fails with` "required net.corda.finance.contracts.asset.Cash.Commands.Move command" tweak { command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } @@ -445,6 +448,7 @@ class CashTests : TestDependencyInjectionBase() { val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) val THEIR_IDENTITY_1 = AnonymousParty(MINI_CORP_PUBKEY) + val THEIR_IDENTITY_2 = AnonymousParty(CHARLIE_PUBKEY) fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = StateAndRef( @@ -462,13 +466,13 @@ class CashTests : TestDependencyInjectionBase() { /** * Generate an exit transaction, removing some amount of cash from the ledger. */ - fun makeExit(amount: Amount, corp: Party, depositRef: Byte = 1): WireTransaction { + private fun makeExit(amount: Amount, corp: Party, depositRef: Byte = 1): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), WALLET) return tx.toWireTransaction() } - fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { + private fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionBuilder(DUMMY_NOTARY) database.transaction { Cash.generateSpend(miniCorpServices, tx, amount, dest) @@ -555,9 +559,11 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateSimpleDirectSpend() { initialiseTestSerialization() + val wtx = + database.transaction { + makeSpend(100.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { - val wtx = makeSpend(100.DOLLARS, THEIR_IDENTITY_1) - @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) @@ -581,10 +587,11 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateSimpleSpendWithChange() { initialiseTestSerialization() + val wtx = + database.transaction { + makeSpend(10.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { - - val wtx = makeSpend(10.DOLLARS, THEIR_IDENTITY_1) - @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) @@ -597,9 +604,11 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateSpendWithTwoInputs() { initialiseTestSerialization() + val wtx = + database.transaction { + makeSpend(500.DOLLARS, THEIR_IDENTITY_1) + } database.transaction { - val wtx = makeSpend(500.DOLLARS, THEIR_IDENTITY_1) - @Suppress("UNCHECKED_CAST") val vaultState0 = vaultStatesUnconsumed.elementAt(0) val vaultState1 = vaultStatesUnconsumed.elementAt(1) @@ -613,20 +622,21 @@ class CashTests : TestDependencyInjectionBase() { @Test fun generateSpendMixedDeposits() { initialiseTestSerialization() + val wtx = + database.transaction { + val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) + assertEquals(3, wtx.inputs.size) + wtx + } database.transaction { - val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) - assertEquals(3, wtx.inputs.size) - - @Suppress("UNCHECKED_CAST") - val vaultState0 = vaultStatesUnconsumed.elementAt(0) - val vaultState1 = vaultStatesUnconsumed.elementAt(1) - @Suppress("UNCHECKED_CAST") - val vaultState2 = vaultStatesUnconsumed.elementAt(2) + val vaultState0: StateAndRef = vaultStatesUnconsumed.elementAt(0) + val vaultState1: StateAndRef = vaultStatesUnconsumed.elementAt(1) + val vaultState2: StateAndRef = vaultStatesUnconsumed.elementAt(2) assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState1.ref, wtx.inputs[1]) assertEquals(vaultState2.ref, wtx.inputs[2]) assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) - assertEquals(vaultState2.state.data.copy(owner = THEIR_IDENTITY_1), wtx.getOutput(0)) + assertEquals(vaultState2.state.data.copy(owner = THEIR_IDENTITY_1), wtx.outputs[0].data) assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @@ -764,4 +774,28 @@ class CashTests : TestDependencyInjectionBase() { this.verifies() } } + + @Test + fun multiSpend() { + initialiseTestSerialization() + val tx = TransactionBuilder(DUMMY_NOTARY) + database.transaction { + val payments = listOf( + PartyAndAmount(THEIR_IDENTITY_1, 400.DOLLARS), + PartyAndAmount(THEIR_IDENTITY_2, 150.DOLLARS) + ) + Cash.generateSpend(miniCorpServices, tx, payments) + } + val wtx = tx.toWireTransaction() + fun out(i: Int) = wtx.getOutput(i) as Cash.State + assertEquals(4, wtx.outputs.size) + assertEquals(80.DOLLARS, out(0).amount.withoutIssuer()) + assertEquals(320.DOLLARS, out(1).amount.withoutIssuer()) + assertEquals(150.DOLLARS, out(2).amount.withoutIssuer()) + assertEquals(30.DOLLARS, out(3).amount.withoutIssuer()) + assertEquals(MINI_CORP, out(0).amount.token.issuer.party) + assertEquals(MEGA_CORP, out(1).amount.token.issuer.party) + assertEquals(MEGA_CORP, out(2).amount.token.issuer.party) + assertEquals(MEGA_CORP, out(3).amount.token.issuer.party) + } } diff --git a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt similarity index 77% rename from finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt rename to finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt index 401b8ae324..9817e20a70 100644 --- a/finance/src/test/kotlin/net/corda/contracts/DummyFungibleContract.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/DummyFungibleContract.kt @@ -1,26 +1,23 @@ -package net.corda.contracts.asset +package net.corda.finance.contracts.asset import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party import net.corda.core.internal.Emoji import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.schemas.SampleCashSchemaV1 -import net.corda.schemas.SampleCashSchemaV2 -import net.corda.schemas.SampleCashSchemaV3 +import net.corda.finance.utils.sumCash +import net.corda.finance.utils.sumCashOrNull +import net.corda.finance.utils.sumCashOrZero +import net.corda.finance.schemas.SampleCashSchemaV1 +import net.corda.finance.schemas.SampleCashSchemaV2 +import net.corda.finance.schemas.SampleCashSchemaV3 import java.security.PublicKey import java.util.* class DummyFungibleContract : OnLedgerAsset() { - override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") - override fun extractCommands(commands: Collection>): List> = commands.select() @@ -36,7 +33,7 @@ class DummyFungibleContract : OnLedgerAsset>, newOwner: AbstractParty): FungibleAsset + override fun withNewOwnerAndAmount(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)" @@ -54,7 +51,7 @@ class DummyFungibleContract : OnLedgerAsset SampleCashSchemaV2.PersistentCashState( - _participants = this.participants.toSet(), + _participants = this.participants.toMutableSet(), _owner = this.owner, _quantity = this.amount.quantity, currency = this.amount.token.product.currencyCode, @@ -62,12 +59,12 @@ class DummyFungibleContract : OnLedgerAsset SampleCashSchemaV3.PersistentCashState( - _participants = this.participants.toSet(), - _owner = this.owner, - _quantity = this.amount.quantity, - _currency = this.amount.token.product.currencyCode, - _issuerParty = this.amount.token.issuer.party, - _issuerRef = this.amount.token.issuer.reference.bytes + participants = this.participants.toMutableSet(), + owner = this.owner, + pennies = this.amount.quantity, + currency = this.amount.token.product.currencyCode, + issuer = this.amount.token.issuer.party, + issuerRef = this.amount.token.issuer.reference.bytes ) else -> throw IllegalArgumentException("Unrecognised schema $schema") } @@ -77,26 +74,19 @@ class DummyFungibleContract : OnLedgerAsset = listOf(SampleCashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) } - interface Commands : FungibleAsset.Commands { + interface Commands : CommandData { - data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands + data class Move(override val contract: Class? = null) : MoveCommand - data class Issue(override val nonce: Long = newSecureRandom().nextLong()) : FungibleAsset.Commands.Issue, Commands + class Issue : TypeOnlyCommandData() - data class Exit(override val amount: Amount>) : Commands, FungibleAsset.Commands.Exit + data class Exit(val amount: Amount>) : CommandData } - fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: AbstractParty, notary: Party) - = generateIssue(tx, Amount(pennies, tokenDef), owner, notary) - - fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) - = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) - override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) - override fun generateIssueCommand() = Commands.Issue() override fun generateMoveCommand() = Commands.Move() override fun verify(tx: LedgerTransaction) { @@ -155,7 +145,6 @@ class DummyFungibleContract : OnLedgerAsset() requireThat { - "the issue command has a nonce" using (issueCommand.value.nonce != 0L) // TODO: This doesn't work with the trader demo, so use the underlying key instead // "output states are issued by a command signer" by (issuer.party in issueCommand.signingParties) "output states are issued by a command signer" using (issuer.party.owningKey in issueCommand.signers) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt similarity index 87% rename from finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt rename to finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt index d661b0f7ab..de7d0e68d5 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/asset/ObligationTests.kt @@ -1,10 +1,8 @@ -package net.corda.contracts.asset +package net.corda.finance.contracts.asset -import net.corda.contracts.Commodity -import net.corda.contracts.NetType -import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sha256 import net.corda.core.crypto.testing.NULL_PARTY import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -13,6 +11,10 @@ import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.days import net.corda.core.utilities.hours +import net.corda.finance.* +import net.corda.finance.contracts.Commodity +import net.corda.finance.contracts.NetType +import net.corda.finance.contracts.asset.Obligation.Lifecycle import net.corda.testing.* import net.corda.testing.contracts.DummyState import net.corda.testing.node.MockServices @@ -27,26 +29,26 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue class ObligationTests { - val defaultRef = OpaqueBytes.of(1) - val defaultIssuer = MEGA_CORP.ref(defaultRef) - val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer - val trustedCashContract = NonEmptySet.of(SecureHash.randomSHA256() as SecureHash) - val megaIssuedDollars = NonEmptySet.of(Issued(defaultIssuer, USD)) - val megaIssuedPounds = NonEmptySet.of(Issued(defaultIssuer, GBP)) - val fivePm: Instant = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS) + 17.hours - val sixPm: Instant = fivePm + 1.hours - val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) - val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) - val inState = Obligation.State( + private val defaultRef = OpaqueBytes.of(1) + private val defaultIssuer = MEGA_CORP.ref(defaultRef) + private val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer + private val trustedCashContract = NonEmptySet.of(SecureHash.randomSHA256() as SecureHash) + private val megaIssuedDollars = NonEmptySet.of(Issued(defaultIssuer, USD)) + private val megaIssuedPounds = NonEmptySet.of(Issued(defaultIssuer, GBP)) + private val fivePm: Instant = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS) + 17.hours + private val sixPm: Instant = fivePm + 1.hours + private val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) + private val megaCorpPoundSettlement = megaCorpDollarSettlement.copy(acceptableIssuedProducts = megaIssuedPounds) + private val inState = Obligation.State( lifecycle = Lifecycle.NORMAL, obligor = MEGA_CORP, template = megaCorpDollarSettlement, quantity = 1000.DOLLARS.quantity, beneficiary = CHARLIE ) - val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) - val miniCorpServices = MockServices(MINI_CORP_KEY) - val notaryServices = MockServices(DUMMY_NOTARY_KEY) + private val outState = inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) + private val miniCorpServices = MockServices(MINI_CORP_KEY) + private val notaryServices = MockServices(DUMMY_NOTARY_KEY) private fun cashObligationTestRoots( group: LedgerDSL @@ -68,16 +70,17 @@ class ObligationTests { fun trivial() { transaction { input { inState } - this `fails with` "the amounts balance" tweak { output { outState.copy(quantity = 2000.DOLLARS.quantity) } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } tweak { output { outState } - // No command arguments - this `fails with` "required net.corda.contracts.asset.Obligation.Commands.Move command" + command(CHARLIE.owningKey) { DummyCommandData } + // Invalid command + this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command" } tweak { output { outState } @@ -126,10 +129,6 @@ class ObligationTests { template = megaCorpDollarSettlement ) } - tweak { - command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue(0) } - this `fails with` "has a nonce" - } command(MINI_CORP_PUBKEY) { Obligation.Commands.Issue() } this.verifies() } @@ -186,7 +185,7 @@ class ObligationTests { this `fails with` "" } - // Can't have any other commands if we have an issue command (because the issue command overrules them) + // Can't have any other commands if we have an issue command (because the issue command overrules them). transaction { input { inState } output { inState.copy(quantity = inState.amount.quantity * 2) } @@ -224,8 +223,8 @@ class ObligationTests { @Test fun `generate close-out net transaction`() { initialiseTestSerialization() - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) + val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) val tx = TransactionBuilder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() @@ -236,8 +235,8 @@ class ObligationTests { @Test fun `generate close-out net transaction with remainder`() { initialiseTestSerialization() - val obligationAliceToBob = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) + val obligationAliceToBob = getStateAndRef((2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB)) + val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) val tx = TransactionBuilder(DUMMY_NOTARY).apply { Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() @@ -251,10 +250,10 @@ class ObligationTests { @Test fun `generate payment net transaction`() { initialiseTestSerialization() - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) + val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + val obligationBobToAlice = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) val tx = TransactionBuilder(DUMMY_NOTARY).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + Obligation().generatePaymentNetting(this, obligationAliceToBob.state.data.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() assertEquals(0, tx.outputs.size) } @@ -263,17 +262,25 @@ class ObligationTests { @Test fun `generate payment net transaction with remainder`() { initialiseTestSerialization() - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) - val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE) - val tx = TransactionBuilder(null).apply { - Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) + val obligationAliceToBob = getStateAndRef(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + val obligationAliceToBobState = obligationAliceToBob.state.data + val obligationBobToAlice = getStateAndRef((2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE)) + val obligationBobToAliceState = obligationBobToAlice.state.data + val tx = TransactionBuilder(DUMMY_NOTARY).apply { + Obligation().generatePaymentNetting(this, obligationAliceToBobState.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) }.toWireTransaction() assertEquals(1, tx.outputs.size) - val expected = obligationBobToAlice.copy(quantity = obligationBobToAlice.quantity - obligationAliceToBob.quantity) + val expected = obligationBobToAliceState.copy(quantity = obligationBobToAliceState.quantity - obligationAliceToBobState.quantity) val actual = tx.getOutput(0) assertEquals(expected, actual) } + private inline fun getStateAndRef(state: T): StateAndRef { + val txState = TransactionState(state, DUMMY_NOTARY) + return StateAndRef(txState, StateRef(SecureHash.randomSHA256(), 0)) + + } + /** Test generating a transaction to mark outputs as having defaulted. */ @Test fun `generate set lifecycle`() { @@ -284,7 +291,7 @@ class ObligationTests { // Generate a transaction issuing the obligation. var tx = TransactionBuilder(null).apply { val amount = Amount(100, Issued(defaultIssuer, USD)) - Obligation().generateCashIssue(this, ALICE, amount, dueBefore, + Obligation().generateCashIssue(this, ALICE, cashContractBytes.sha256(), amount, dueBefore, beneficiary = MINI_CORP, notary = DUMMY_NOTARY) } var stx = miniCorpServices.signInitialTransaction(tx) @@ -302,7 +309,7 @@ class ObligationTests { stx.verifyRequiredSignatures() // And set it back - stateAndRef = stx.tx.outRef>(0) + stateAndRef = stx.tx.outRef(0) tx = TransactionBuilder(DUMMY_NOTARY).apply { Obligation().generateSetLifecycle(this, listOf(stateAndRef), Lifecycle.NORMAL, DUMMY_NOTARY) } @@ -459,7 +466,8 @@ class ObligationTests { input("Alice's $1,000,000") output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } + attachment(attachment(cashContractBytes.inputStream())) this.verifies() } } @@ -473,7 +481,8 @@ class ObligationTests { output("Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB) } output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } + attachment(attachment(cashContractBytes.inputStream())) this.verifies() } } @@ -486,7 +495,7 @@ class ObligationTests { input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } this `fails with` "all inputs are in the normal state" } } @@ -499,7 +508,8 @@ class ObligationTests { input("Alice's $1,000,000") output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } - command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation::class.java) } + attachment(attachment(cashContractBytes.inputStream())) this `fails with` "amount in settle command" } } @@ -507,9 +517,10 @@ class ObligationTests { @Test fun `commodity settlement`() { + val commodityContractBytes = "https://www.big-book-of-banking-law.gov/commodity-claims.html".toByteArray() val defaultFcoj = Issued(defaultIssuer, Commodity.getInstance("FCOJ")!!) val oneUnitFcoj = Amount(1, defaultFcoj) - val obligationDef = Obligation.Terms(NonEmptySet.of(CommodityContract().legalContractReference), NonEmptySet.of(defaultFcoj), TEST_TX_TIME) + val obligationDef = Obligation.Terms(NonEmptySet.of(commodityContractBytes.sha256() as SecureHash), NonEmptySet.of(defaultFcoj), TEST_TX_TIME) val oneUnitFcojObligation = Obligation.State(Obligation.Lifecycle.NORMAL, ALICE, obligationDef, oneUnitFcoj.quantity, NULL_PARTY) // Try settling a simple commodity obligation @@ -523,7 +534,8 @@ class ObligationTests { input("Alice's 1 FCOJ") output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB) } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)) } - command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation().legalContractReference) } + command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation::class.java) } + attachment(attachment(commodityContractBytes.inputStream())) verifies() } } @@ -596,15 +608,20 @@ class ObligationTests { @Test fun zeroSizedValues() { transaction { - input { inState } - input { inState.copy(quantity = 0L) } - this `fails with` "zero sized inputs" - } - transaction { - input { inState } - output { inState } - output { inState.copy(quantity = 0L) } - this `fails with` "zero sized outputs" + command(CHARLIE.owningKey) { Obligation.Commands.Move() } + tweak { + input { inState } + input { inState.copy(quantity = 0L) } + + this `fails with` "zero sized inputs" + } + tweak { + input { inState } + output { inState } + output { inState.copy(quantity = 0L) } + + this `fails with` "zero sized outputs" + } } } @@ -614,6 +631,7 @@ class ObligationTests { transaction { input { inState } output { outState `issued by` MINI_CORP } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } // Can't mix currencies. @@ -621,6 +639,7 @@ class ObligationTests { input { inState } output { outState.copy(quantity = 80000, template = megaCorpDollarSettlement) } output { outState.copy(quantity = 20000, template = megaCorpPoundSettlement) } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } transaction { @@ -633,6 +652,7 @@ class ObligationTests { ) } output { outState.copy(quantity = 115000) } + command(MINI_CORP_PUBKEY) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } // Can't have superfluous input states from different issuers. @@ -660,7 +680,7 @@ class ObligationTests { tweak { command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) } - this `fails with` "required net.corda.contracts.asset.Obligation.Commands.Move command" + this `fails with` "required net.corda.finance.contracts.asset.Obligation.Commands.Move command" tweak { command(CHARLIE.owningKey) { Obligation.Commands.Move() } @@ -703,12 +723,14 @@ class ObligationTests { // Can't merge them together. tweak { output { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY), quantity = 200000L) } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } // Missing MiniCorp deposit tweak { output { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) } output { inState.copy(beneficiary = AnonymousParty(BOB_PUBKEY)) } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } @@ -884,8 +906,9 @@ class ObligationTests { assertEquals(expected, actual) } - val Issued.OBLIGATION_DEF: Obligation.Terms - get() = Obligation.Terms(NonEmptySet.of(Cash().legalContractReference), NonEmptySet.of(this), TEST_TX_TIME) - val Amount>.OBLIGATION: Obligation.State + private val cashContractBytes = "https://www.big-book-of-banking-law.gov/cash-claims.html".toByteArray() + private val Issued.OBLIGATION_DEF: Obligation.Terms + get() = Obligation.Terms(NonEmptySet.of(cashContractBytes.sha256() as SecureHash), NonEmptySet.of(this), TEST_TX_TIME) + private val Amount>.OBLIGATION: Obligation.State get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) } diff --git a/finance/src/test/kotlin/net/corda/contracts/math/InterpolatorsTest.kt b/finance/src/test/kotlin/net/corda/finance/contracts/math/InterpolatorsTest.kt similarity index 98% rename from finance/src/test/kotlin/net/corda/contracts/math/InterpolatorsTest.kt rename to finance/src/test/kotlin/net/corda/finance/contracts/math/InterpolatorsTest.kt index 1041a4507d..57d4de1b97 100644 --- a/finance/src/test/kotlin/net/corda/contracts/math/InterpolatorsTest.kt +++ b/finance/src/test/kotlin/net/corda/finance/contracts/math/InterpolatorsTest.kt @@ -1,4 +1,4 @@ -package net.corda.contracts.math +package net.corda.finance.contracts.math import org.junit.Assert import org.junit.Test diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt similarity index 85% rename from finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt rename to finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt index ed967899a2..454d7bcf22 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashExitFlowTests.kt @@ -1,11 +1,11 @@ -package net.corda.flows +package net.corda.finance.flows -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.`issued by` import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -33,9 +33,7 @@ class CashExitFlowTests { bankOfCorda = bankOfCordaNode.info.legalIdentity mockNet.runNetwork() - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, - bankOfCorda, - notary)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture mockNet.runNetwork() future.getOrThrow() } @@ -48,8 +46,7 @@ class CashExitFlowTests { @Test fun `exit some cash`() { val exitAmount = 500.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, - ref)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture mockNet.runNetwork() val exitTx = future.getOrThrow().stx.tx val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) @@ -62,8 +59,7 @@ class CashExitFlowTests { @Test fun `exit zero cash`() { val expected = 0.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, - ref)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, ref)).resultFuture mockNet.runNetwork() assertFailsWith { future.getOrThrow() diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt similarity index 84% rename from finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt rename to finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt index 962632c4fc..92ba4e8ae6 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashIssueFlowTests.kt @@ -1,11 +1,11 @@ -package net.corda.flows +package net.corda.finance.flows -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.`issued by` import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode @@ -42,9 +42,7 @@ class CashIssueFlowTests { fun `issue some cash`() { val expected = 500.DOLLARS val ref = OpaqueBytes.of(0x01) - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, - bankOfCorda, - notary)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture mockNet.runNetwork() val issueTx = future.getOrThrow().stx val output = issueTx.tx.outputsOfType().single() @@ -54,9 +52,8 @@ class CashIssueFlowTests { @Test fun `issue zero cash`() { val expected = 0.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, OpaqueBytes.of(0x01), - bankOfCorda, - notary)).resultFuture + val ref = OpaqueBytes.of(0x01) + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, notary)).resultFuture mockNet.runNetwork() assertFailsWith { future.getOrThrow() diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt similarity index 94% rename from finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt rename to finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt index e056758ffb..0e2171f603 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/finance/flows/CashPaymentFlowTests.kt @@ -1,14 +1,14 @@ -package net.corda.flows +package net.corda.finance.flows -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.`issued by` import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.trackBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash import net.corda.testing.expect import net.corda.testing.expectEvents import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin @@ -37,9 +37,7 @@ class CashPaymentFlowTests { notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, - bankOfCorda, - notary)).resultFuture + val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, notary)).resultFuture mockNet.runNetwork() future.getOrThrow() } diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt similarity index 87% rename from finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt index 453dff0203..f9395c1adb 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV1.kt @@ -1,8 +1,11 @@ -package net.corda.schemas +package net.corda.finance.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState -import javax.persistence.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Index +import javax.persistence.Table /** * An object used to fully qualify the [CashSchema] family name (i.e. independent of version). diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt similarity index 85% rename from finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt index 83755a4110..7a5e0da89c 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV2.kt @@ -1,8 +1,8 @@ -package net.corda.schemas +package net.corda.finance.schemas import net.corda.core.identity.AbstractParty -import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.CommonSchemaV1 +import net.corda.core.schemas.MappedSchema import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Index @@ -13,7 +13,7 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 2, - mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) { + mappedTypes = listOf(PersistentCashState::class.java)) { @Entity @Table(name = "cash_states_v2", indexes = arrayOf(Index(name = "ccy_code_idx2", columnList = "ccy_code"))) @@ -33,5 +33,5 @@ object SampleCashSchemaV2 : MappedSchema(schemaFamily = CashSchema.javaClass, ve val _issuerParty: AbstractParty, @Transient val _issuerRef: ByteArray - ) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef) + ) : CommonSchemaV1.FungibleState(_participants.toMutableSet(), _owner, _quantity, _issuerParty, _issuerRef) } diff --git a/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt new file mode 100644 index 0000000000..f6288c0591 --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCashSchemaV3.kt @@ -0,0 +1,43 @@ +package net.corda.finance.schemas + +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import javax.persistence.Column +import javax.persistence.ElementCollection +import javax.persistence.Entity +import javax.persistence.Table + +/** + * First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood + * at the time of writing. + */ +object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 3, + mappedTypes = listOf(PersistentCashState::class.java)) { + @Entity + @Table(name = "cash_states_v3") + class PersistentCashState( + /** [ContractState] attributes */ + + /** X500Name of participant parties **/ + @ElementCollection + var participants: MutableSet? = null, + + /** X500Name of owner party **/ + @Column(name = "owner_name") + var owner: AbstractParty, + + @Column(name = "pennies") + var pennies: Long, + + @Column(name = "ccy_code", length = 3) + var currency: String, + + /** X500Name of issuer party **/ + @Column(name = "issuer_name") + var issuer: AbstractParty, + + @Column(name = "issuer_ref") + var issuerRef: ByteArray + ) : PersistentState() +} diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV1.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt similarity index 98% rename from finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV1.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt index b45075a9eb..9b4c91b179 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV1.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV1.kt @@ -1,4 +1,4 @@ -package net.corda.schemas +package net.corda.finance.schemas import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt similarity index 85% rename from finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt rename to finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt index 735eda7afc..ead55b4b1e 100644 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCommercialPaperSchemaV2.kt +++ b/finance/src/test/kotlin/net/corda/finance/schemas/SampleCommercialPaperSchemaV2.kt @@ -1,8 +1,8 @@ -package net.corda.schemas +package net.corda.finance.schemas import net.corda.core.identity.AbstractParty -import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.CommonSchemaV1 +import net.corda.core.schemas.MappedSchema import java.time.Instant import javax.persistence.Column import javax.persistence.Entity @@ -14,7 +14,7 @@ import javax.persistence.Table * [VaultFungibleState] abstract schema */ object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPaperSchema.javaClass, version = 1, - mappedTypes = listOf(PersistentCommercialPaperState::class.java, CommonSchemaV1.Party::class.java)) { + mappedTypes = listOf(PersistentCommercialPaperState::class.java)) { @Entity @Table(name = "cp_states_v2", indexes = arrayOf(Index(name = "ccy_code_index2", columnList = "ccy_code"), @@ -44,5 +44,5 @@ object SampleCommercialPaperSchemaV2 : MappedSchema(schemaFamily = CommercialPap val _issuerParty: AbstractParty, @Transient val _issuerRef: ByteArray - ) : CommonSchemaV1.FungibleState(_participants, _owner, _quantity, _issuerParty, _issuerRef) + ) : CommonSchemaV1.FungibleState(_participants.toMutableSet(), _owner, _quantity, _issuerParty, _issuerRef) } diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt deleted file mode 100644 index 2edfca7884..0000000000 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ /dev/null @@ -1,168 +0,0 @@ -package net.corda.flows - -import net.corda.contracts.asset.Cash -import net.corda.core.concurrent.CordaFuture -import net.corda.testing.contracts.calculateRandomlySizedAmounts -import net.corda.core.contracts.Amount -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.currency -import net.corda.core.flows.FlowException -import net.corda.core.identity.Party -import net.corda.core.node.services.Vault -import net.corda.core.node.services.trackBy -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.utilities.OpaqueBytes -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.getOrThrow -import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.testing.expect -import net.corda.testing.expectEvents -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNetwork.MockNode -import net.corda.testing.sequence -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import java.util.* -import kotlin.test.assertFailsWith - -@RunWith(Parameterized::class) -class IssuerFlowTest(val anonymous: Boolean) { - companion object { - @Parameterized.Parameters - @JvmStatic - fun data(): Collection> { - return listOf(arrayOf(false), arrayOf(true)) - } - } - - lateinit var mockNet: MockNetwork - lateinit var notaryNode: MockNode - lateinit var bankOfCordaNode: MockNode - lateinit var bankClientNode: MockNode - - @Before - fun start() { - mockNet = MockNetwork(threadPerNode = true) - val basketOfNodes = mockNet.createSomeNodes(2) - bankOfCordaNode = basketOfNodes.partyNodes[0] - bankClientNode = basketOfNodes.partyNodes[1] - notaryNode = basketOfNodes.notaryNode - } - - @After - fun cleanUp() { - mockNet.stopNodes() - } - - @Test - fun `test issuer flow`() { - val notary = notaryNode.services.myInfo.notaryIdentity - val (vaultUpdatesBoc, vaultUpdatesBankClient) = bankOfCordaNode.database.transaction { - // Register for vault updates - val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) - val (_, vaultUpdatesBankClient) = bankClientNode.services.vaultQueryService.trackBy(criteria) - - // using default IssueTo Party Reference - val issuerResult = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, - bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) - issuerResult.get() - - Pair(vaultUpdatesBoc, vaultUpdatesBankClient) - } - - // Check Bank of Corda Vault Updates - vaultUpdatesBoc.expectEvents { - sequence( - // ISSUE - expect { update -> - require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } - require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } - val issued = update.produced.single().state.data - require(issued.owner.owningKey in bankOfCordaNode.services.keyManagementService.keys) - }, - // MOVE - expect { update -> - require(update.consumed.size == 1) { "Expected 1 consumed states, actual: $update" } - require(update.produced.isEmpty()) { "Expected 0 produced states, actual: $update" } - } - ) - } - - // Check Bank Client Vault Updates - vaultUpdatesBankClient.expectEvents { - // MOVE - expect { (consumed, produced) -> - require(consumed.isEmpty()) { consumed.size } - require(produced.size == 1) { produced.size } - val paidState = produced.single().state.data - require(paidState.owner.owningKey in bankClientNode.services.keyManagementService.keys) - } - } - } - - @Test - fun `test issuer flow rejects restricted`() { - val notary = notaryNode.services.myInfo.notaryIdentity - // try to issue an amount of a restricted currency - assertFailsWith { - runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() - } - } - - @Test - fun `test issue flow to self`() { - val notary = notaryNode.services.myInfo.notaryIdentity - val vaultUpdatesBoc = bankOfCordaNode.database.transaction { - val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val (_, vaultUpdatesBoc) = bankOfCordaNode.services.vaultQueryService.trackBy(criteria) - - // using default IssueTo Party Reference - runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, - bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123), notary).getOrThrow() - vaultUpdatesBoc - } - - // Check Bank of Corda Vault Updates - vaultUpdatesBoc.expectEvents { - sequence( - // ISSUE - expect { update -> - require(update.consumed.isEmpty()) { "Expected 0 consumed states, actual: $update" } - require(update.produced.size == 1) { "Expected 1 produced states, actual: $update" } - } - ) - } - } - - @Test - fun `test concurrent issuer flow`() { - val notary = notaryNode.services.myInfo.notaryIdentity - // this test exercises the Cashflow issue and move subflows to ensure consistent spending of issued states - val amount = 10000.DOLLARS - val amounts = calculateRandomlySizedAmounts(10000.DOLLARS, 10, 10, Random()) - val handles = amounts.map { pennies -> - runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123), notary) - } - handles.forEach { - require(it.get().stx is SignedTransaction) - } - } - - private fun runIssuerAndIssueRequester(issuerNode: MockNode, - issueToNode: MockNode, - amount: Amount, - issueToParty: Party, - ref: OpaqueBytes, - notaryParty: Party): CordaFuture { - val issueToPartyAndRef = issueToParty.ref(ref) - val issueRequest = IssuanceRequester(amount, issueToParty, issueToPartyAndRef.reference, issuerNode.info.legalIdentity, notaryParty, - anonymous) - return issueToNode.services.startFlow(issueRequest).resultFuture - } -} \ No newline at end of file diff --git a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt b/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt deleted file mode 100644 index d1d7e46d79..0000000000 --- a/finance/src/test/kotlin/net/corda/schemas/SampleCashSchemaV3.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.corda.schemas - -import net.corda.core.identity.AbstractParty -import net.corda.core.schemas.MappedSchema -import net.corda.core.schemas.PersistentState -import net.corda.core.schemas.CommonSchemaV1 -import javax.persistence.* - -/** - * First version of a cash contract ORM schema that maps all fields of the [Cash] contract state as it stood - * at the time of writing. - */ -object SampleCashSchemaV3 : MappedSchema(schemaFamily = CashSchema.javaClass, version = 3, - mappedTypes = listOf(PersistentCashState::class.java, CommonSchemaV1.Party::class.java)) { - @Entity - @Table(name = "cash_states_v3") - class PersistentCashState( - /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, - - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var owner: CommonSchemaV1.Party, - - @Column(name = "pennies") - var pennies: Long, - - @Column(name = "ccy_code", length = 3) - var currency: String, - - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var issuerParty: CommonSchemaV1.Party, - - @Column(name = "issuer_ref") - var issuerRef: ByteArray - ) : PersistentState() { - constructor(_participants: Set, _owner: AbstractParty, _quantity: Long, _currency: String, _issuerParty: AbstractParty, _issuerRef: ByteArray) - : this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(), - owner = CommonSchemaV1.Party(_owner), - pennies = _quantity, - currency = _currency, - issuerParty = CommonSchemaV1.Party(_issuerParty), - issuerRef = _issuerRef) - } -} diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index edd74182f2..59b495879e 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -76,7 +76,7 @@ class Cordformation implements Plugin { "This can cause node stability problems. Please use 'corda' instead." + "See http://docs.corda.net/cordapp-build-systems.html") } else { - logger.trace("Including dependency: $it") + logger.info("Including dependency in CorDapp JAR: $it") } } return filteredDeps.collect { configurations.runtime.files it }.flatten().toSet() diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index fb2ea88550..f530928695 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -235,6 +235,8 @@ class Node extends CordformNode { * @return List of this node's cordapps. */ private Collection getCordappList() { + // Cordapps can sometimes contain a GString instance which fails the equality test with the Java string + List cordapps = this.cordapps.collect { it.toString() } return project.configurations.cordapp.files { cordapps.contains(it.group + ":" + it.name + ":" + it.version) } diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index 4a9711572e..7ec608802a 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -3,10 +3,10 @@ package net.corda.plugins import java.awt.GraphicsEnvironment import java.io.File import java.nio.file.Files -import java.nio.file.Path import java.util.* private val HEADLESS_FLAG = "--headless" +private val CAPSULE_DEBUG_FLAG = "--capsule-debug" private val os by lazy { val osName = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH) @@ -24,13 +24,15 @@ private object debugPortAlloc { fun main(args: Array) { val startedProcesses = mutableListOf() - val headless = GraphicsEnvironment.isHeadless() || (args.isNotEmpty() && args[0] == HEADLESS_FLAG) + val headless = GraphicsEnvironment.isHeadless() || args.contains(HEADLESS_FLAG) + val capsuleDebugMode = args.contains(CAPSULE_DEBUG_FLAG) val workingDir = File(System.getProperty("user.dir")) - val javaArgs = args.filter { it != HEADLESS_FLAG } + val javaArgs = args.filter { it != HEADLESS_FLAG && it != CAPSULE_DEBUG_FLAG } + val jvmArgs = if (capsuleDebugMode) listOf("-Dcapsule.log=verbose") else emptyList() println("Starting nodes in $workingDir") workingDir.listFiles { file -> file.isDirectory }.forEach { dir -> listOf(NodeJarType, WebJarType).forEach { jarType -> - jarType.acceptDirAndStartProcess(dir, headless, javaArgs)?.let { startedProcesses += it } + jarType.acceptDirAndStartProcess(dir, headless, javaArgs, jvmArgs)?.let { startedProcesses += it } } } println("Started ${startedProcesses.size} processes") @@ -39,7 +41,7 @@ fun main(args: Array) { private abstract class JarType(private val jarName: String) { internal abstract fun acceptNodeConf(nodeConf: File): Boolean - internal fun acceptDirAndStartProcess(dir: File, headless: Boolean, javaArgs: List): Process? { + internal fun acceptDirAndStartProcess(dir: File, headless: Boolean, javaArgs: List, jvmArgs: List): Process? { if (!File(dir, jarName).exists()) { return null } @@ -48,7 +50,7 @@ private abstract class JarType(private val jarName: String) { } val debugPort = debugPortAlloc.next() println("Starting $jarName in $dir on debug port $debugPort") - val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, javaArgs).start() + val process = (if (headless) ::HeadlessJavaCommand else ::TerminalWindowJavaCommand)(jarName, dir, debugPort, javaArgs, jvmArgs).start() if (os == OS.MACOS) Thread.sleep(1000) return process } @@ -63,9 +65,17 @@ private object WebJarType : JarType("corda-webserver.jar") { override fun acceptNodeConf(nodeConf: File) = Files.lines(nodeConf.toPath()).anyMatch { "webAddress" in it } } -private abstract class JavaCommand(jarName: String, internal val dir: File, debugPort: Int?, internal val nodeName: String, init: MutableList.() -> Unit, args: List) { +private abstract class JavaCommand( + jarName: String, + internal val dir: File, + debugPort: Int?, + internal val nodeName: String, + init: MutableList.() -> Unit, args: List, + jvmArgs: List +) { internal val command: List = mutableListOf().apply { add(getJavaPath()) + addAll(jvmArgs) add("-Dname=$nodeName") null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") add("-jar") @@ -79,12 +89,14 @@ private abstract class JavaCommand(jarName: String, internal val dir: File, debu internal abstract fun getJavaPath(): String } -private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List) : JavaCommand(jarName, dir, debugPort, dir.name, { add("--no-local-shell") }, args) { +private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List, jvmArgs: List) + : JavaCommand(jarName, dir, debugPort, dir.name, { add("--no-local-shell") }, args, jvmArgs) { override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path } -private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List) : JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args) { +private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List, jvmArgs: List) + : JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args, jvmArgs) { override fun processBuilder() = ProcessBuilder(when (os) { OS.MACOS -> { listOf("osascript", "-e", """tell app "Terminal" @@ -113,7 +125,7 @@ end tell""") val path = File(File(System.getProperty("java.home"), "bin"), "java").path // Replace below is to fix an issue with spaces in paths on Windows. // Quoting the entire path does not work, only the space or directory within the path. - return if(os == OS.WINDOWS) path.replace(" ", "\" \"") else path + return if (os == OS.WINDOWS) path.replace(" ", "\" \"") else path } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 48d4660db6..7a3265ee94 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 224fc8b276..383fa73d26 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Mar 21 18:12:23 GMT 2017 +#Thu Aug 24 12:32:31 BST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/gradlew b/gradlew index 4453ccea33..cccdd3d517 100755 --- a/gradlew +++ b/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/node-api/build.gradle b/node-api/build.gradle index 43d289940b..a0844f5f13 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -31,6 +31,9 @@ dependencies { compile "com.esotericsoftware:kryo:4.0.0" compile "de.javakaffee:kryo-serializers:0.41" + // For AMQP serialisation. + compile "org.apache.qpid:proton-j:0.19.0" + // Unit testing helpers. testCompile "junit:junit:$junit_version" testCompile "org.assertj:assertj-core:${assertj_version}" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt index a4be40c829..d99f17dab7 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -6,10 +6,8 @@ import com.esotericsoftware.kryo.Registration import com.esotericsoftware.kryo.Serializer import net.corda.core.concurrent.CordaFuture import net.corda.core.CordaRuntimeException -import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory import net.corda.core.toFuture import net.corda.core.toObservable import net.corda.nodeapi.config.OldConfig @@ -49,7 +47,7 @@ class PermissionException(msg: String) : RuntimeException(msg) // The Kryo used for the RPC wire protocol. Every type in the wire protocol is listed here explicitly. // This is annoying to write out, but will make it easier to formalise the wire protocol when the time comes, // because we can see everything we're using in one place. -class RPCKryo(observableSerializer: Serializer>, val serializationFactory: SerializationFactory, val serializationContext: SerializationContext) : CordaKryo(CordaClassResolver(serializationFactory, serializationContext)) { +class RPCKryo(observableSerializer: Serializer>, serializationContext: SerializationContext) : CordaKryo(CordaClassResolver(serializationContext)) { init { DefaultKryoCustomizer.customize(this) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt index 3d1ca75f34..d4a38efe6a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AMQPSerializationScheme.kt @@ -11,7 +11,7 @@ import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory import java.util.concurrent.ConcurrentHashMap -internal val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferedSerializationVersion == AmqpHeaderV1_0 +internal val AMQP_ENABLED get() = SerializationDefaults.P2P_CONTEXT.preferredSerializationVersion == AmqpHeaderV1_0 abstract class AbstractAMQPSerializationScheme : SerializationScheme { internal companion object { @@ -22,7 +22,23 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { register(net.corda.nodeapi.internal.serialization.amqp.custom.X500NameSerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) register(net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer) + register(net.corda.nodeapi.internal.serialization.amqp.custom.OpaqueBytesSubSequenceSerializer(this)) register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.DurationSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateTimeSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalTimeSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.ZonedDateTimeSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.ZoneIdSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetTimeSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetDateTimeSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.YearSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.YearMonthSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.MonthDaySerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.PeriodSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.ClassSerializer(this)) + register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CertificateHolderSerializer) + register(net.corda.nodeapi.internal.serialization.amqp.custom.PartyAndCertificateSerializer(factory)) } } } @@ -41,7 +57,7 @@ abstract class AbstractAMQPSerializationScheme : SerializationScheme { rpcClientSerializerFactory(context) SerializationContext.UseCase.RPCServer -> rpcServerSerializerFactory(context) - else -> SerializerFactory(context.whitelist) // TODO pass class loader also + else -> SerializerFactory(context.whitelist, context.deserializationClassLoader) } }.also { registerCustomSerializers(it) } } @@ -99,9 +115,3 @@ val AMQP_P2P_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0, emptyMap(), true, SerializationContext.UseCase.P2P) -val AMQP_STORAGE_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0, - SerializationDefaults.javaClass.classLoader, - AllButBlacklisted, - emptyMap(), - true, - SerializationContext.UseCase.Storage) \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt index 81d72f0989..eadd539414 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/AllButBlacklisted.kt @@ -39,6 +39,9 @@ object AllButBlacklisted : ClassWhitelist { Thread::class.java.name, HashSet::class.java.name, HashMap::class.java.name, + WeakHashMap::class.java.name, + Dictionary::class.java.name, // Deprecated (marked obsolete) in the jdk + Hashtable::class.java.name, // see [Dictionary] ClassLoader::class.java.name, Handler::class.java.name, // MemoryHandler, StreamHandler Runtime::class.java.name, diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt new file mode 100644 index 0000000000..add6b1465e --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ClientContexts.kt @@ -0,0 +1,17 @@ +@file:JvmName("ClientContexts") +package net.corda.nodeapi.internal.serialization + +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationDefaults + +/* + * Serialisation contexts for the client. + * These have been refactored into a separate file to prevent + * servers from trying to instantiate any of them. + */ +val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, + SerializationDefaults.javaClass.classLoader, + GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), + emptyMap(), + true, + SerializationContext.UseCase.RPCClient) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt index 9c180148fd..de21b06ac0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolver.kt @@ -8,7 +8,6 @@ import com.esotericsoftware.kryo.util.DefaultClassResolver import com.esotericsoftware.kryo.util.Util import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor -import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 import java.io.PrintWriter import java.lang.reflect.Modifier.isAbstract import java.nio.charset.StandardCharsets @@ -22,10 +21,9 @@ fun Kryo.addToWhitelist(type: Class<*>) { } /** - * @param amqpEnabled Setting this to true turns on experimental AMQP serialization for any class annotated with - * [CordaSerializable]. + * Corda specific class resolver which enables extra customisation for the purposes of serialization using Kryo */ -class CordaClassResolver(val serializationFactory: SerializationFactory, val serializationContext: SerializationContext) : DefaultClassResolver() { +class CordaClassResolver(serializationContext: SerializationContext) : DefaultClassResolver() { val whitelist: ClassWhitelist = TransientClassWhiteList(serializationContext.whitelist) /** Returns the registration for the specified class, or null if the class is not registered. */ @@ -64,13 +62,6 @@ class CordaClassResolver(val serializationFactory: SerializationFactory, val ser } override fun registerImplicit(type: Class<*>): Registration { - // If something is not annotated, or AMQP is disabled, we stay serializing with Kryo. This will typically be the - // case for flow checkpoints (ignoring all cases where AMQP is disabled) since our top level messaging data structures - // are annotated and once we enter AMQP serialisation we stay with it for the entire object subgraph. - if (checkForAnnotation(type) && AMQP_ENABLED) { - // Build AMQP serializer - return register(Registration(type, KryoAMQPSerializer(serializationFactory, serializationContext), NAME.toInt())) - } val objectInstance = try { type.kotlin.objectInstance @@ -82,13 +73,12 @@ class CordaClassResolver(val serializationFactory: SerializationFactory, val ser val references = kryo.references try { kryo.references = true - val serializer = if (objectInstance != null) { - KotlinObjectSerializer(objectInstance) - } else if (kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type)) { - // Kotlin lambdas extend this class and any captured variables are stored in synthentic fields - FieldSerializer(kryo, type).apply { setIgnoreSyntheticFields(false) } - } else { - kryo.getDefaultSerializer(type) + val serializer = when { + objectInstance != null -> KotlinObjectSerializer(objectInstance) + kotlin.jvm.internal.Lambda::class.java.isAssignableFrom(type) -> // Kotlin lambdas extend this class and any captured variables are stored in synthetic fields + FieldSerializer(kryo, type).apply { setIgnoreSyntheticFields(false) } + Throwable::class.java.isAssignableFrom(type) -> ThrowableSerializer(kryo, type) + else -> kryo.getDefaultSerializer(type) } return register(Registration(type, serializer, NAME.toInt())) } finally { @@ -125,11 +115,9 @@ class CordaClassResolver(val serializationFactory: SerializationFactory, val ser // TODO: come up with a more efficient way. e.g. segregate the name space by class loader. if (nameToClass != null) { val classesToRemove: MutableList = ArrayList(nameToClass.size) - for (entry in nameToClass.entries()) { - if (entry.value.classLoader is AttachmentsClassLoader) { - classesToRemove += entry.key - } - } + nameToClass.entries() + .filter { it.value.classLoader is AttachmentsClassLoader } + .forEach { classesToRemove += it.key } for (className in classesToRemove) { nameToClass.remove(className) } @@ -169,7 +157,7 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass } /** - * A whitelist that can be customised via the [CordaPluginRegistry], since implements [MutableClassWhitelist]. + * A whitelist that can be customised via the [net.corda.core.node.CordaPluginRegistry], since implements [MutableClassWhitelist]. */ class TransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate { val whitelist: MutableSet = Collections.synchronizedSet(mutableSetOf()) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt index 5112ea6a33..9cb3b8ea57 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/DefaultKryoCustomizer.kt @@ -6,12 +6,13 @@ import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer import com.esotericsoftware.kryo.serializers.FieldSerializer -import com.esotericsoftware.kryo.util.MapReferenceResolver import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.guava.* +import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.composite.CompositeKey +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializedBytes @@ -59,6 +60,12 @@ object DefaultKryoCustomizer { instantiatorStrategy = CustomInstantiatorStrategy() + // Required for HashCheckingStream (de)serialization. + // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). + addDefaultSerializer(InputStream::class.java, InputStreamSerializer) + addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) + addDefaultSerializer(Logger::class.java, LoggerSerializer) + // WARNING: reordering the registrations here will cause a change in the serialized form, since classes // with custom serializers get written as registration ids. This will break backwards-compatibility. // Please add any new registrations to the end. @@ -68,50 +75,31 @@ object DefaultKryoCustomizer { register(SignedTransaction::class.java, SignedTransactionSerializer) register(WireTransaction::class.java, WireTransactionSerializer) register(SerializedBytes::class.java, SerializedBytesSerializer) - UnmodifiableCollectionsSerializer.registerSerializers(this) ImmutableListSerializer.registerSerializers(this) ImmutableSetSerializer.registerSerializers(this) ImmutableSortedSetSerializer.registerSerializers(this) ImmutableMapSerializer.registerSerializers(this) ImmutableMultimapSerializer.registerSerializers(this) - // InputStream subclasses whitelisting, required for attachments. register(BufferedInputStream::class.java, InputStreamSerializer) register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer) - noReferencesWithin() - register(ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer) register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) - - // Using a custom serializer for compactness - register(CompositeKey::class.java, CompositeKeySerializer) - + register(CompositeKey::class.java, CompositeKeySerializer) // Using a custom serializer for compactness // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway. register(Array::class, read = { _, _ -> emptyArray() }, write = { _, _, _ -> }) - // This ensures a NonEmptySetSerializer is constructed with an initial value. register(NonEmptySet::class.java, NonEmptySetSerializer) - - addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer()) - register(BitSet::class.java, BitSetSerializer()) register(Class::class.java, ClassSerializer) - - addDefaultSerializer(Logger::class.java, LoggerSerializer) - register(FileInputStream::class.java, InputStreamSerializer) - // Required for HashCheckingStream (de)serialization. - // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). - addDefaultSerializer(InputStream::class.java, InputStreamSerializer) - register(CertPath::class.java, CertPathSerializer) register(X509CertPath::class.java, CertPathSerializer) register(X500Name::class.java, X500NameSerializer) register(X509CertificateHolder::class.java, X509CertificateSerializer) - register(BCECPrivateKey::class.java, PrivateKeySerializer) register(BCECPublicKey::class.java, PublicKeySerializer) register(BCRSAPrivateCrtKey::class.java, PrivateKeySerializer) @@ -119,8 +107,11 @@ object DefaultKryoCustomizer { register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer) register(BCSphincs256PublicKey::class.java, PublicKeySerializer) register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) - register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer) + register(PartyAndCertificate::class.java, PartyAndCertificateSerializer) + + // Don't deserialize PrivacySalt via its default constructor. + register(PrivacySalt::class.java, PrivacySaltSerializer) val customization = KryoSerializationCustomization(this) pluginRegistries.forEach { it.customizeSerialization(customization) } @@ -139,6 +130,15 @@ object DefaultKryoCustomizer { } } + private object PartyAndCertificateSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: PartyAndCertificate) { + kryo.writeClassAndObject(output, obj.certPath) + } + override fun read(kryo: Kryo, input: Input, type: Class): PartyAndCertificate { + return PartyAndCertificate(kryo.readClassAndObject(input) as CertPath) + } + } + private object NonEmptySetSerializer : Serializer>() { override fun write(kryo: Kryo, output: Output, obj: NonEmptySet) { // Write out the contents as normal @@ -156,4 +156,18 @@ object DefaultKryoCustomizer { return list.toNonEmptySet() } } -} \ No newline at end of file + + /* + * Avoid deserialising PrivacySalt via its default constructor + * because the random number generator may not be available. + */ + private object PrivacySaltSerializer : Serializer() { + override fun write(kryo: Kryo, output: Output, obj: PrivacySalt) { + output.writeBytesWithLength(obj.bytes) + } + + override fun read(kryo: Kryo, input: Input, type: Class): PrivacySalt { + return PrivacySalt(input.readBytesWithLength()) + } + } +} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt index 9066dbed7e..7bd6886278 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/Kryo.kt @@ -1,8 +1,11 @@ package net.corda.nodeapi.internal.serialization import com.esotericsoftware.kryo.* +import com.esotericsoftware.kryo.factories.ReflectionSerializerFactory import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output +import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer +import com.esotericsoftware.kryo.serializers.FieldSerializer import com.esotericsoftware.kryo.util.MapReferenceResolver import net.corda.core.contracts.* import net.corda.core.crypto.Crypto @@ -569,4 +572,46 @@ object X509CertificateSerializer : Serializer() { } } -fun Kryo.serializationContext(): SerializeAsTokenContext? = context.get(serializationContextKey) as? SerializeAsTokenContext \ No newline at end of file +fun Kryo.serializationContext(): SerializeAsTokenContext? = context.get(serializationContextKey) as? SerializeAsTokenContext + +/** + * For serializing instances if [Throwable] honoring the fact that [java.lang.Throwable.suppressedExceptions] + * might be un-initialized/empty. + * In the absence of this class [CompatibleFieldSerializer] will be used which will assign a *new* instance of + * unmodifiable collection to [java.lang.Throwable.suppressedExceptions] which will fail some sentinel identity checks + * e.g. in [java.lang.Throwable.addSuppressed] + */ +@ThreadSafe +class ThrowableSerializer(kryo: Kryo, type: Class) : Serializer(false, true) { + + private companion object { + private val suppressedField = Throwable::class.java.getDeclaredField("suppressedExceptions") + + private val sentinelValue = let { + val sentinelField = Throwable::class.java.getDeclaredField("SUPPRESSED_SENTINEL") + sentinelField.isAccessible = true + sentinelField.get(null) + } + + init { + suppressedField.isAccessible = true + } + } + + @Suppress("UNCHECKED_CAST") + private val delegate: Serializer = ReflectionSerializerFactory.makeSerializer(kryo, FieldSerializer::class.java, type) as Serializer + + override fun write(kryo: Kryo, output: Output, throwable: Throwable) { + delegate.write(kryo, output, throwable) + } + + override fun read(kryo: Kryo, input: Input, type: Class): Throwable { + val throwableRead = delegate.read(kryo, input, type) + if(throwableRead.suppressed.isEmpty()) { + throwableRead.setSuppressedToSentinel() + } + return throwableRead + } + + private fun Throwable.setSuppressedToSentinel() = suppressedField.set(this, sentinelValue) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt deleted file mode 100644 index 16dec8a83e..0000000000 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/KryoAMQPSerializer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.corda.nodeapi.internal.serialization - -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.Serializer -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output -import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory -import net.corda.core.serialization.SerializedBytes -import net.corda.core.utilities.sequence -import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 -import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput -import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput -import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory - -/** - * This [Kryo] custom [Serializer] switches the object graph of anything annotated with `@CordaSerializable` - * to using the AMQP serialization wire format, and simply writes that out as bytes to the wire. - * - * There is no need to write out the length, since this can be peeked out of the first few bytes of the stream. - */ -class KryoAMQPSerializer(val serializationFactory: SerializationFactory, val serializationContext: SerializationContext) : Serializer() { - override fun write(kryo: Kryo, output: Output, obj: Any) { - val bytes = serializationFactory.serialize(obj, serializationContext.withPreferredSerializationVersion(AmqpHeaderV1_0)).bytes - // No need to write out the size since it's encoded within the AMQP. - output.write(bytes) - } - - override fun read(kryo: Kryo, input: Input, type: Class): Any { - // Use our helper functions to peek the size of the serialized object out of the AMQP byte stream. - val peekedBytes = input.readBytes(DeserializationInput.BYTES_NEEDED_TO_PEEK) - val size = DeserializationInput.peekSize(peekedBytes) - val allBytes = peekedBytes.copyOf(size) - input.readBytes(allBytes, peekedBytes.size, size - peekedBytes.size) - return serializationFactory.deserialize(allBytes.sequence(), type, serializationContext) - } -} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt index 4ab8c3c27e..f08f66e9fb 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/SerializationScheme.kt @@ -8,7 +8,6 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool -import io.requery.util.CloseableIterator import net.corda.core.internal.LazyPool import net.corda.core.serialization.* import net.corda.core.utilities.ByteSequence @@ -28,7 +27,7 @@ object NotSupportedSeralizationScheme : SerializationScheme { override fun serialize(obj: T, context: SerializationContext): SerializedBytes = doThrow() } -data class SerializationContextImpl(override val preferedSerializationVersion: ByteSequence, +data class SerializationContextImpl(override val preferredSerializationVersion: ByteSequence, override val deserializationClassLoader: ClassLoader, override val whitelist: ClassWhitelist, override val properties: Map, @@ -52,7 +51,7 @@ data class SerializationContextImpl(override val preferedSerializationVersion: B }) } - override fun withPreferredSerializationVersion(versionHeader: ByteSequence) = copy(preferedSerializationVersion = versionHeader) + override fun withPreferredSerializationVersion(versionHeader: ByteSequence) = copy(preferredSerializationVersion = versionHeader) } private const val HEADER_SIZE: Int = 8 @@ -68,11 +67,9 @@ open class SerializationFactoryImpl : SerializationFactory { private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): SerializationScheme { // truncate sequence to 8 bytes, and make sure it's a copy to avoid holding onto large ByteArrays return schemes.computeIfAbsent(byteSequence.take(HEADER_SIZE).copy() to target) { - for (scheme in registeredSchemes) { - if (scheme.canDeserializeVersion(it.first, it.second)) { - return@computeIfAbsent scheme - } - } + registeredSchemes + .filter { scheme -> scheme.canDeserializeVersion(it.first, it.second) } + .forEach { return@computeIfAbsent it } NotSupportedSeralizationScheme } } @@ -81,7 +78,7 @@ open class SerializationFactoryImpl : SerializationFactory { override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T = schemeFor(byteSequence, context.useCase).deserialize(byteSequence, clazz, context) override fun serialize(obj: T, context: SerializationContext): SerializedBytes { - return schemeFor(context.preferedSerializationVersion, context.useCase).serialize(obj, context) + return schemeFor(context.preferredSerializationVersion, context.useCase).serialize(obj, context) } fun registerScheme(scheme: SerializationScheme) { @@ -105,21 +102,16 @@ open class SerializationFactoryImpl : SerializationFactory { private object AutoCloseableSerialisationDetector : Serializer() { override fun write(kryo: Kryo, output: Output, closeable: AutoCloseable) { - val message = if (closeable is CloseableIterator<*>) { - "A live Iterator pointing to the database has been detected during flow checkpointing. This may be due " + - "to a Vault query - move it into a private method." - } else { - "${closeable.javaClass.name}, which is a closeable resource, has been detected during flow checkpointing. " + + val message = "${closeable.javaClass.name}, which is a closeable resource, has been detected during flow checkpointing. " + "Restoring such resources across node restarts is not supported. Make sure code accessing it is " + "confined to a private method or the reference is nulled out." - } throw UnsupportedOperationException(message) } override fun read(kryo: Kryo, input: Input, type: Class) = throw IllegalStateException("Should not reach here!") } -abstract class AbstractKryoSerializationScheme(val serializationFactory: SerializationFactory) : SerializationScheme { +abstract class AbstractKryoSerializationScheme : SerializationScheme { private val kryoPoolsForContexts = ConcurrentHashMap, KryoPool>() protected abstract fun rpcClientKryoPool(context: SerializationContext): KryoPool @@ -131,7 +123,7 @@ abstract class AbstractKryoSerializationScheme(val serializationFactory: Seriali SerializationContext.UseCase.Checkpoint -> KryoPool.Builder { val serializer = Fiber.getFiberSerializer(false) as KryoSerializer - val classResolver = CordaClassResolver(serializationFactory, context).apply { setKryo(serializer.kryo) } + val classResolver = CordaClassResolver(context).apply { setKryo(serializer.kryo) } // TODO The ClassResolver can only be set in the Kryo constructor and Quasar doesn't provide us with a way of doing that val field = Kryo::class.java.getDeclaredField("classResolver").apply { isAccessible = true } serializer.kryo.apply { @@ -147,7 +139,7 @@ abstract class AbstractKryoSerializationScheme(val serializationFactory: Seriali rpcServerKryoPool(context) else -> KryoPool.Builder { - DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(serializationFactory, context))).apply { classLoader = it.second } + DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(context))).apply { classLoader = it.second } }.build() } } @@ -226,34 +218,6 @@ val KRYO_P2P_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, emptyMap(), true, SerializationContext.UseCase.P2P) -val KRYO_RPC_SERVER_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, - SerializationDefaults.javaClass.classLoader, - GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), - emptyMap(), - true, - SerializationContext.UseCase.RPCServer) -val KRYO_RPC_CLIENT_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, - SerializationDefaults.javaClass.classLoader, - GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), - emptyMap(), - true, - SerializationContext.UseCase.RPCClient) -val KRYO_STORAGE_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, - SerializationDefaults.javaClass.classLoader, - AllButBlacklisted, - emptyMap(), - true, - SerializationContext.UseCase.Storage) -val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, - SerializationDefaults.javaClass.classLoader, - QuasarWhitelist, - emptyMap(), - true, - SerializationContext.UseCase.Checkpoint) - -object QuasarWhitelist : ClassWhitelist { - override fun hasListed(type: Class<*>): Boolean = true -} interface SerializationScheme { // byteSequence expected to just be the 8 bytes necessary for versioning diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt new file mode 100644 index 0000000000..89dd6b524d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/ServerContexts.kt @@ -0,0 +1,46 @@ +@file:JvmName("ServerContexts") +package net.corda.nodeapi.internal.serialization + +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.SerializationContext +import net.corda.core.serialization.SerializationDefaults +import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0 + +/* + * Serialisation contexts for the server. + * These have been refactored into a separate file to prevent + * clients from trying to instantiate any of them. + * + * NOTE: The [KRYO_STORAGE_CONTEXT] and [AMQP_STORAGE_CONTEXT] + * CANNOT always be instantiated outside of the server and so + * MUST be kept separate! + */ +val KRYO_RPC_SERVER_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, + SerializationDefaults.javaClass.classLoader, + GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()), + emptyMap(), + true, + SerializationContext.UseCase.RPCServer) +val KRYO_STORAGE_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, + SerializationDefaults.javaClass.classLoader, + AllButBlacklisted, + emptyMap(), + true, + SerializationContext.UseCase.Storage) +val KRYO_CHECKPOINT_CONTEXT = SerializationContextImpl(KryoHeaderV0_1, + SerializationDefaults.javaClass.classLoader, + QuasarWhitelist, + emptyMap(), + true, + SerializationContext.UseCase.Checkpoint) + +object QuasarWhitelist : ClassWhitelist { + override fun hasListed(type: Class<*>): Boolean = true +} + +val AMQP_STORAGE_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0, + SerializationDefaults.javaClass.classLoader, + AllButBlacklisted, + emptyMap(), + true, + SerializationContext.UseCase.Storage) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt index 2b8660ed45..5d218a1a2a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.utilities.NonEmptySet import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException import java.lang.reflect.ParameterizedType @@ -22,7 +23,8 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali List::class.java to { list -> Collections.unmodifiableList(list) }, Set::class.java to { list -> Collections.unmodifiableSet(LinkedHashSet(list)) }, SortedSet::class.java to { list -> Collections.unmodifiableSortedSet(TreeSet(list)) }, - NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) } + NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) }, + NonEmptySet::class.java to { list -> NonEmptySet.copyOf(list) } ) private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt index 58752b0ecc..032c453751 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.SerializationDefaults import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.nameForType import org.apache.qpid.proton.codec.Data import java.lang.reflect.Type @@ -13,7 +14,7 @@ abstract class CustomSerializer : AMQPSerializer { * This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects * that refer to other custom types etc. */ - abstract val additionalSerializers: Iterable> + open val additionalSerializers: Iterable> = emptyList() /** * This method should return true if the custom serializer can serialize an instance of the class passed as the @@ -44,7 +45,6 @@ abstract class CustomSerializer : AMQPSerializer { */ // TODO: should this be a custom serializer at all, or should it just be a plain AMQPSerializer? class SubClass(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer) : CustomSerializer() { - override val additionalSerializers: Iterable> = emptyList() // TODO: should this be empty or contain the schema of the super? override val schemaForDocumentation = Schema(emptyList()) @@ -68,26 +68,26 @@ abstract class CustomSerializer : AMQPSerializer { } /** - * Additional base features for a custom serializer for a particular class, that excludes subclasses. + * Additional base features for a custom serializer for a particular class [withInheritance] is false + * or super class / interfaces [withInheritance] is true */ - abstract class Is(protected val clazz: Class) : CustomSerializer() { - override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz + abstract class CustomSerializerImp(protected val clazz: Class, protected val withInheritance: Boolean) : CustomSerializer() { override val type: Type get() = clazz override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" override fun writeClassInfo(output: SerializationOutput) {} override val descriptor: Descriptor = Descriptor(typeDescriptor) + override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz } + /** + * Additional base features for a custom serializer for a particular class, that excludes subclasses. + */ + abstract class Is(clazz: Class) : CustomSerializerImp(clazz, false) + /** * Additional base features for a custom serializer for all implementations of a particular interface or super class. */ - abstract class Implements(protected val clazz: Class) : CustomSerializer() { - override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz) - override val type: Type get() = clazz - override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" - override fun writeClassInfo(output: SerializationOutput) {} - override val descriptor: Descriptor = Descriptor(typeDescriptor) - } + abstract class Implements(clazz: Class) : CustomSerializerImp(clazz, true) /** * Additional base features over and above [Implements] or [Is] custom serializer for when the serialized form should be @@ -96,15 +96,11 @@ abstract class CustomSerializer : AMQPSerializer { * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered * custom serializers. */ - abstract class Proxy(protected val clazz: Class, + abstract class Proxy(clazz: Class, protected val proxyClass: Class

, protected val factory: SerializerFactory, - val withInheritance: Boolean = true) : CustomSerializer() { + withInheritance: Boolean = true) : CustomSerializerImp(clazz, withInheritance) { override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz - override val type: Type get() = clazz - override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}" - override fun writeClassInfo(output: SerializationOutput) {} - override val descriptor: Descriptor = Descriptor(typeDescriptor) private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) } @@ -151,25 +147,25 @@ abstract class CustomSerializer : AMQPSerializer { * @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method. */ abstract class ToString(clazz: Class, withInheritance: Boolean = false, - private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> { string -> `constructor`.newInstance(string) } }, - private val unmaker: (T) -> String = { obj -> obj.toString() }) : Proxy(clazz, String::class.java, /* Unused */ SerializerFactory(), withInheritance) { + private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { + `constructor` -> + { string -> `constructor`.newInstance(string) } + }, + private val unmaker: (T) -> String = { obj -> obj.toString() }) + : CustomSerializerImp(clazz, withInheritance) { - override val additionalSerializers: Iterable> = emptyList() - - override val schemaForDocumentation = Schema(listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)), SerializerFactory.primitiveTypeName(String::class.java)!!, descriptor, emptyList()))) - - override fun toProxy(obj: T): String = unmaker(obj) - - override fun fromProxy(proxy: String): T = maker(proxy) + override val schemaForDocumentation = Schema( + listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)), + SerializerFactory.primitiveTypeName(String::class.java)!!, + descriptor, emptyList()))) override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) { - val proxy = toProxy(obj) - data.putObject(proxy) + data.putObject(unmaker(obj)) } override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { val proxy = input.readObject(obj, schema, String::class.java) as String - return fromProxy(proxy) + return maker(proxy) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt index 5654dfa20d..612d196433 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializationInput.kt @@ -8,6 +8,7 @@ import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedByte import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException +import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.nio.ByteBuffer import java.util.* @@ -20,7 +21,7 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -class DeserializationInput(internal val serializerFactory: SerializerFactory = SerializerFactory()) { +class DeserializationInput(internal val serializerFactory: SerializerFactory) { // TODO: we're not supporting object refs yet private val objectHistory: MutableList = ArrayList() @@ -118,7 +119,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S if (obj is DescribedType) { // Look up serializer in factory by descriptor val serializer = serializerFactory.get(obj.descriptor, schema) - if (serializer.type != type && !serializer.type.isSubClassOf(type)) + if (serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) }) throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " + "expected to be of type $type but was ${serializer.type}") return serializer.readObject(obj.described, schema, this) @@ -128,4 +129,12 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S return obj } } -} + + /** + * TODO: Currently performs rather basic checks aimed in particular at [java.util.List>] and + * [java.lang.Class] + * In the future tighter control might be needed + */ + private fun Type.materiallyEquivalentTo(that: Type): Boolean = + asClass() == that.asClass() && that is ParameterizedType +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt index 5da78fabeb..3209866821 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedType.kt @@ -19,25 +19,17 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p if (params.size != rawType.typeParameters.size) { throw NotSerializableException("Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}") } - // We do not check bounds. Both our use cases (Collection and Map) are not bounded. - if (rawType.typeParameters.any { boundedType(it) }) throw NotSerializableException("Bounded types in ParameterizedTypes not supported, but found a bound in $rawType") } private fun boundedType(type: TypeVariable>): Boolean { return !(type.bounds.size == 1 && type.bounds[0] == Object::class.java) } - val isFullyWildcarded: Boolean = params.all { it == SerializerFactory.AnyType } - private val _typeName: String = makeTypeName() private fun makeTypeName(): String { - return if (isFullyWildcarded) { - rawType.name - } else { - val paramsJoined = params.map { it.typeName }.joinToString(", ") - "${rawType.name}<$paramsJoined>" - } + val paramsJoined = params.map { it.typeName }.joinToString(", ") + return "${rawType.name}<$paramsJoined>" } companion object { @@ -96,7 +88,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p typeStart = pos++ } else if (!needAType) { throw NotSerializableException("Not expecting a type") - } else if (params[pos] == '*') { + } else if (params[pos] == '?') { pos++ } else if (!params[pos].isJavaIdentifierStart()) { throw NotSerializableException("Invalid character at start of type: ${params[pos]}") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt new file mode 100644 index 0000000000..2632b4b46d --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolutionSerializer.kt @@ -0,0 +1,130 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.DeprecatedConstructorForDeserialization +import net.corda.nodeapi.internal.serialization.carpenter.getTypeAsClass +import org.apache.qpid.proton.codec.Data +import java.lang.reflect.Type +import java.io.NotSerializableException +import kotlin.reflect.KFunction +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.javaType + +/** + * Serializer for deserialising objects whose definition has changed since they + * were serialised + */ +class EvolutionSerializer( + clazz: Type, + factory: SerializerFactory, + val readers: List, + override val kotlinConstructor: KFunction?) : ObjectSerializer(clazz, factory) { + + // explicitly set as empty to indicate it's unused by this type of serializer + override val propertySerializers: Collection = emptyList() + + /** + * Represents a parameter as would be passed to the constructor of the class as it was + * when it was serialised and NOT how that class appears now + * + * @param type The jvm type of the parameter + * @param idx where in the parameter list this parameter falls. Required as the parameter + * order may have been changed and we need to know where into the list to look + * @param property object to read the actual property value + */ + data class OldParam(val type: Type, val idx: Int, val property: PropertySerializer) { + fun readProperty(paramValues: List<*>, schema: Schema, input: DeserializationInput) = + property.readProperty(paramValues[idx], schema, input) + } + + companion object { + /** + * Unlike the generic deserialisation case where we need to locate the primary constructor + * for the object (or our best guess) in the case of an object whose structure has changed + * since serialisation we need to attempt to locate a constructor that we can use. I.e. + * it's parameters match the serialised members and it will initialise any newly added + * elements + * + * TODO: Type evolution + * TODO: rename annotation + */ + internal fun getEvolverConstructor(type: Type, oldArgs: Map): KFunction? { + val clazz: Class<*> = type.asClass()!! + if (!isConcrete(clazz)) return null + + val oldArgumentSet = oldArgs.map { Pair(it.key, it.value) } + + var maxConstructorVersion = Integer.MIN_VALUE + var constructor: KFunction? = null + clazz.kotlin.constructors.forEach { + val version = it.findAnnotation()?.version ?: Integer.MIN_VALUE + if (oldArgumentSet.containsAll(it.parameters.map { v -> Pair(v.name, v.type.javaType) }) && + version > maxConstructorVersion) { + constructor = it + maxConstructorVersion = version + } + } + + // if we didn't get an exact match revert to existing behaviour, if the new parameters + // are not mandatory (i.e. nullable) things are fine + return constructor ?: constructorForDeserialization(type) + } + + /** + * Build a serialization object for deserialisation only of objects serialised + * as different versions of a class + * + * @param old is an object holding the schema that represents the object + * as it was serialised and the type descriptor of that type + * @param new is the Serializer built for the Class as it exists now, not + * how it was serialised and persisted. + */ + fun make(old: CompositeType, new: ObjectSerializer, + factory: SerializerFactory): AMQPSerializer { + + val oldFieldToType = old.fields.map { + it.name as String? to it.getTypeAsClass(factory.classloader) as Type + }.toMap() + + val constructor = getEvolverConstructor(new.type, oldFieldToType) ?: + throw NotSerializableException( + "Attempt to deserialize an interface: ${new.type}. Serialized form is invalid.") + + val oldArgs = mutableMapOf() + var idx = 0 + old.fields.forEach { + val returnType = it.getTypeAsClass(factory.classloader) + oldArgs[it.name] = OldParam( + returnType, idx++, PropertySerializer.make(it.name, null, returnType, factory)) + } + + val readers = constructor.parameters.map { + oldArgs[it.name!!] ?: if (!it.type.isMarkedNullable) { + throw NotSerializableException( + "New parameter ${it.name} is mandatory, should be nullable for evolution to worK") + } else null + } + + return EvolutionSerializer(new.type, factory, readers, constructor) + } + } + + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + throw IllegalAccessException("It should be impossible to write an evolution serializer") + } + + /** + * Unlike a normal [readObject] call where we simply apply the parameter deserialisers + * to the object list of values we need to map that list, which is ordered per the + * constructor of the original state of the object, we need to map the new parameter order + * of the current constructor onto that list inserting nulls where new parameters are + * encountered + * + * TODO: Object references + */ + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { + if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj") + + return construct(readers.map { it?.readProperty(obj, schema, input) }) + } +} + diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt index c399243445..8b74459fe5 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt @@ -5,6 +5,7 @@ import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* +import kotlin.collections.LinkedHashMap import kotlin.collections.Map import kotlin.collections.iterator import kotlin.collections.map @@ -17,12 +18,15 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" companion object { - private val supportedTypes: Map>, (Map<*, *>) -> Map<*, *>> = mapOf( + private val supportedTypes: Map, (Map<*, *>) -> Map<*, *>> = mapOf( + // Interfaces Map::class.java to { map -> Collections.unmodifiableMap(map) }, SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) }, - NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) } + NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) }, + // concrete classes for user convenience + LinkedHashMap::class.java to { map -> LinkedHashMap(map) }, + TreeMap::class.java to { map -> TreeMap(map) } ) - private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> { return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") } @@ -40,7 +44,7 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact } override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { - obj.javaClass.checkNotUnorderedHashMap() + obj.javaClass.checkNotUnsupportedHashMap() // Write described data.withDescribed(typeNotation.descriptor) { // Write map @@ -65,8 +69,17 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1]) } -internal fun Class<*>.checkNotUnorderedHashMap() { +internal fun Class<*>.checkNotUnsupportedHashMap() { if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) { - throw IllegalArgumentException("Map type $this is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.") + throw IllegalArgumentException( + "Map type $this is unstable under iteration. Suggested fix: use java.util.LinkedHashMap instead.") + } + else if (WeakHashMap::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException ("Weak references with map types not supported. Suggested fix: " + + "use java.util.LinkedHashMap instead.") + } + else if (Dictionary::class.java.isAssignableFrom(this)) { + throw IllegalArgumentException ( + "Unable to serialise deprecated type $this. Suggested fix: prefer java.util.map implementations") } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt index c942b2e6de..3b99e84371 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt @@ -4,30 +4,27 @@ import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion import org.apache.qpid.proton.amqp.UnsignedInteger import org.apache.qpid.proton.codec.Data import java.io.NotSerializableException -import java.lang.reflect.Constructor import java.lang.reflect.Type import kotlin.reflect.jvm.javaConstructor /** * Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor). */ -class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer { +open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer { override val type: Type get() = clazz - private val javaConstructor: Constructor? - internal val propertySerializers: Collection + open val kotlinConstructor = constructorForDeserialization(clazz) + val javaConstructor by lazy { kotlinConstructor?.javaConstructor } - init { - val kotlinConstructor = constructorForDeserialization(clazz) - javaConstructor = kotlinConstructor?.javaConstructor - propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory) + open internal val propertySerializers: Collection by lazy { + propertiesForSerialization(kotlinConstructor, clazz, factory) } private val typeName = nameForType(clazz) override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" - private val interfaces = interfacesForSerialization(clazz) // TODO maybe this proves too much and we need annotations to restrict. + private val interfaces = interfacesForSerialization(clazz, factory) // We restrict to only those annotated or whitelisted - internal val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) + open internal val typeNotation : TypeNotation by lazy {CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) } override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { @@ -67,15 +64,8 @@ class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerial return propertySerializers.map { Field(it.name, it.type, it.requires, it.default, null, it.mandatory, false) } } - private fun generateProvides(): List { - return interfaces.map { nameForType(it) } - } + private fun generateProvides(): List = interfaces.map { nameForType(it) } - - fun construct(properties: List): Any { - if (javaConstructor == null) { + fun construct(properties: List) = javaConstructor?.newInstance(*properties.toTypedArray()) ?: throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.") - } - return javaConstructor.newInstance(*properties.toTypedArray()) - } } \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt index 143cd16c5b..cc5d07f095 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/PropertySerializer.kt @@ -10,7 +10,7 @@ import kotlin.reflect.jvm.javaGetter /** * Base class for serialization of a property of an object. */ -sealed class PropertySerializer(val name: String, val readMethod: Method, val resolvedType: Type) { +sealed class PropertySerializer(val name: String, val readMethod: Method?, val resolvedType: Type) { abstract fun writeClassInfo(output: SerializationOutput) abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? @@ -44,7 +44,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re } private fun generateMandatory(): Boolean { - return isJVMPrimitive || !readMethod.returnsNullable() + return isJVMPrimitive || !(readMethod?.returnsNullable() ?: true) } private fun Method.returnsNullable(): Boolean { @@ -53,7 +53,8 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re } companion object { - fun make(name: String, readMethod: Method, resolvedType: Type, factory: SerializerFactory): PropertySerializer { + fun make(name: String, readMethod: Method?, resolvedType: Type, factory: SerializerFactory): PropertySerializer { + readMethod?.isAccessible = true if (SerializerFactory.isPrimitive(resolvedType)) { return when(resolvedType) { Char::class.java, Character::class.java -> AMQPCharPropertySerializer(name, readMethod) @@ -68,7 +69,10 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re /** * A property serializer for a complex type (another object). */ - class DescribedTypePropertySerializer(name: String, readMethod: Method, resolvedType: Type, private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) { + class DescribedTypePropertySerializer( + name: String, readMethod: Method?, + resolvedType: Type, + private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) { // This is lazy so we don't get an infinite loop when a method returns an instance of the class. private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() } @@ -83,14 +87,14 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { - output.writeObjectOrNull(readMethod.invoke(obj), data, resolvedType) + output.writeObjectOrNull(readMethod!!.invoke(obj), data, resolvedType) } } /** * A property serializer for most AMQP primitive type (Int, String, etc). */ - class AMQPPrimitivePropertySerializer(name: String, readMethod: Method, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) { + class AMQPPrimitivePropertySerializer(name: String, readMethod: Method?, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) { override fun writeClassInfo(output: SerializationOutput) {} override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { @@ -98,7 +102,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { - val value = readMethod.invoke(obj) + val value = readMethod!!.invoke(obj) if (value is ByteArray) { data.putObject(Binary(value)) } else { @@ -112,7 +116,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re * value of the character is stored in numeric UTF-16 form and on deserialisation requires explicit * casting back to a char otherwise it's treated as an Integer and a TypeMismatch occurs */ - class AMQPCharPropertySerializer(name: String, readMethod: Method) : + class AMQPCharPropertySerializer(name: String, readMethod: Method?) : PropertySerializer(name, readMethod, Character::class.java) { override fun writeClassInfo(output: SerializationOutput) {} @@ -121,7 +125,7 @@ sealed class PropertySerializer(val name: String, val readMethod: Method, val re } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { - val input = readMethod.invoke(obj) + val input = readMethod!!.invoke(obj) if (input != null) data.putShort((input as Char).toShort()) else data.putNull() } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt index a67cb8e400..e82df6443e 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt @@ -4,15 +4,13 @@ import com.google.common.hash.Hasher import com.google.common.hash.Hashing import net.corda.core.crypto.toBase64 import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.loggerFor import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.UnsignedLong import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.DescribedTypeConstructor import java.io.NotSerializableException -import java.lang.reflect.GenericArrayType -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.TypeVariable +import java.lang.reflect.* import java.util.* import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField @@ -316,6 +314,9 @@ private val NULLABLE_HASH: String = "Nullable = true" private val NOT_NULLABLE_HASH: String = "Nullable = false" private val ANY_TYPE_HASH: String = "Any type = true" private val TYPE_VARIABLE_HASH: String = "Type variable = true" +private val WILDCARD_TYPE_HASH: String = "Wild card = true" + +private val logger by lazy { loggerFor() } /** * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation. @@ -338,6 +339,16 @@ internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String { return hasher.hash().asBytes().toBase64() } +private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFactory, clazz: Class<*>, declaredType: Type, block: () -> Hasher) : Hasher { + // Need to check if a custom serializer is applicable + val customSerializer = factory.findCustomSerializer(clazz, declaredType) + return if (customSerializer != null) { + putUnencodedChars(customSerializer.typeDescriptor) + } else { + block() + } +} + // This method concatentates various elements of the types recursively as unencoded strings into the hasher, effectively // creating a unique string for a type which we then hash in the calling function above. private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet, hasher: Hasher, factory: SerializerFactory): Hasher { @@ -356,18 +367,14 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta } else if (isCollectionOrMap(type)) { hasher.putUnencodedChars(type.name) } else { - // Need to check if a custom serializer is applicable - val customSerializer = factory.findCustomSerializer(type, type) - if (customSerializer == null) { + hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) { if (type.kotlin.objectInstance != null) { // TODO: name collision is too likely for kotlin objects, we need to introduce some reference // to the CorDapp but maybe reference to the JAR in the short term. hasher.putUnencodedChars(type.name) } else { - fingerprintForObject(type, contextType, alreadySeen, hasher, factory) + fingerprintForObject(type, type, alreadySeen, hasher, factory) } - } else { - hasher.putUnencodedChars(customSerializer.typeDescriptor) } } } else if (type is ParameterizedType) { @@ -376,7 +383,9 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta val startingHash = if (isCollectionOrMap(clazz)) { hasher.putUnencodedChars(clazz.name) } else { - fingerprintForObject(type, type, alreadySeen, hasher, factory) + hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) { + fingerprintForObject(type, type, alreadySeen, hasher, factory) + } } // ... and concatentate the type data for each parameter type. type.actualTypeArguments.fold(startingHash) { orig, paramType -> fingerprintForType(paramType, type, alreadySeen, orig, factory) } @@ -386,11 +395,16 @@ private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: Muta } else if (type is TypeVariable<*>) { // TODO: include bounds hasher.putUnencodedChars(type.name).putUnencodedChars(TYPE_VARIABLE_HASH) - } else { + } else if (type is WildcardType) { + hasher.putUnencodedChars(type.typeName).putUnencodedChars(WILDCARD_TYPE_HASH) + } + else { throw NotSerializableException("Don't know how to hash") } } catch(e: NotSerializableException) { - throw NotSerializableException("${e.message} -> $type") + val msg = "${e.message} -> $type" + logger.error(msg, e) + throw NotSerializableException(msg) } } } @@ -403,6 +417,6 @@ private fun fingerprintForObject(type: Type, contextType: Type?, alreadySeen: Mu propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory).fold(hasher.putUnencodedChars(name)) { orig, prop -> fingerprintForType(prop.resolvedType, type, alreadySeen, orig, factory).putUnencodedChars(prop.name).putUnencodedChars(if (prop.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) } - interfacesForSerialization(type).map { fingerprintForType(it, type, alreadySeen, hasher, factory) } + interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory) } return hasher } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt index a8c15461ca..f1a356a01d 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationHelper.kt @@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp import com.google.common.reflect.TypeToken import org.apache.qpid.proton.codec.Data +import java.beans.IndexedPropertyDescriptor import java.beans.Introspector import java.io.NotSerializableException import java.lang.reflect.* @@ -64,7 +65,7 @@ internal fun propertiesForSerialization(kotlinConstructor: KFunction): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers)) +fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers)) private fun propertiesForSerializationFromConstructor(kotlinConstructor: KFunction, type: Type, factory: SerializerFactory): Collection { val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType @@ -73,13 +74,14 @@ private fun propertiesForSerializationFromConstructor(kotlinConstructo val rc: MutableList = ArrayList(kotlinConstructor.parameters.size) for (param in kotlinConstructor.parameters) { val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.") - val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz." + + val matchingProperty = properties[name] ?: + throw NotSerializableException("No property matching constructor parameter named $name of $clazz." + " If using Java, check that you have the -parameters option specified in the Java compiler.") // Check that the method has a getter in java. val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." + " If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.") val returnType = resolveTypeVariables(getter.genericReturnType, type) - if (constructorParamTakesReturnTypeOfGetter(getter, param)) { + if (constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param)) { rc += PropertySerializer.make(name, getter, returnType, factory) } else { throw NotSerializableException("Property type $returnType for $name of $clazz differs from constructor parameter type ${param.type.javaType}") @@ -88,11 +90,14 @@ private fun propertiesForSerializationFromConstructor(kotlinConstructo return rc } -private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType) +private fun constructorParamTakesReturnTypeOfGetter(getterReturnType: Type, rawGetterReturnType: Type, param: KParameter): Boolean { + val typeToken = TypeToken.of(param.type.javaType) + return typeToken.isSupertypeOf(getterReturnType) || typeToken.isSupertypeOf(rawGetterReturnType) +} private fun propertiesForSerializationFromAbstract(clazz: Class<*>, type: Type, factory: SerializerFactory): Collection { // Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans. - val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name } + val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name }.filterNot { it is IndexedPropertyDescriptor } val rc: MutableList = ArrayList(properties.size) for (property in properties) { // Check that the method has a getter in java. @@ -103,23 +108,26 @@ private fun propertiesForSerializationFromAbstract(clazz: Class<*>, type: Type, return rc } -internal fun interfacesForSerialization(type: Type): List { +internal fun interfacesForSerialization(type: Type, serializerFactory: SerializerFactory): List { val interfaces = LinkedHashSet() - exploreType(type, interfaces) + exploreType(type, interfaces, serializerFactory) return interfaces.toList() } -private fun exploreType(type: Type?, interfaces: MutableSet) { +private fun exploreType(type: Type?, interfaces: MutableSet, serializerFactory: SerializerFactory) { val clazz = type?.asClass() if (clazz != null) { - if (clazz.isInterface) interfaces += type + if (clazz.isInterface) { + if(serializerFactory.isNotWhitelisted(clazz)) return // We stop exploring once we reach a branch that has no `CordaSerializable` annotation or whitelisting. + else interfaces += type + } for (newInterface in clazz.genericInterfaces) { if (newInterface !in interfaces) { - exploreType(resolveTypeVariables(newInterface, type), interfaces) + exploreType(resolveTypeVariables(newInterface, type), interfaces, serializerFactory) } } val superClass = clazz.genericSuperclass ?: return - exploreType(resolveTypeVariables(superClass, type), interfaces) + exploreType(resolveTypeVariables(superClass, type), interfaces, serializerFactory) } } @@ -159,21 +167,20 @@ private fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { } internal fun Type.asClass(): Class<*>? { - return if (this is Class<*>) { - this - } else if (this is ParameterizedType) { - this.rawType.asClass() - } else if (this is GenericArrayType) { - this.genericComponentType.asClass()?.arrayClass() - } else null + return when { + this is Class<*> -> this + this is ParameterizedType -> this.rawType.asClass() + this is GenericArrayType -> this.genericComponentType.asClass()?.arrayClass() + else -> null + } } internal fun Type.asArray(): Type? { - return if (this is Class<*>) { - this.arrayClass() - } else if (this is ParameterizedType) { - DeserializedGenericArrayType(this) - } else null + return when { + this is Class<*> -> this.arrayClass() + this is ParameterizedType -> DeserializedGenericArrayType(this) + else -> null + } } internal fun Class<*>.arrayClass(): Class<*> = java.lang.reflect.Array.newInstance(this, 0).javaClass diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt index 90011f149c..39b96b9679 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutput.kt @@ -14,7 +14,7 @@ import kotlin.collections.LinkedHashSet * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -open class SerializationOutput(internal val serializerFactory: SerializerFactory = SerializerFactory()) { +open class SerializationOutput(internal val serializerFactory: SerializerFactory) { // TODO: we're not supporting object refs yet private val objectHistory: MutableMap = IdentityHashMap() private val serializerHistory: MutableSet> = LinkedHashSet() diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt index 9b20b08a5c..c0b8b19c3f 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt @@ -4,11 +4,7 @@ import com.google.common.primitives.Primitives import com.google.common.reflect.TypeResolver import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.CordaSerializable -import net.corda.nodeapi.internal.serialization.AllWhitelist -import net.corda.nodeapi.internal.serialization.carpenter.CarpenterSchemas -import net.corda.nodeapi.internal.serialization.carpenter.ClassCarpenter -import net.corda.nodeapi.internal.serialization.carpenter.MetaCarpenter -import net.corda.nodeapi.internal.serialization.carpenter.carpenterSchema +import net.corda.nodeapi.internal.serialization.carpenter.* import org.apache.qpid.proton.amqp.* import java.io.NotSerializableException import java.lang.reflect.GenericArrayType @@ -20,6 +16,8 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import javax.annotation.concurrent.ThreadSafe +data class schemaAndDescriptor (val schema: Schema, val typeDescriptor: Any) + /** * Factory of serializers designed to be shared across threads and invocations. */ @@ -32,9 +30,6 @@ import javax.annotation.concurrent.ThreadSafe // TODO: profile for performance in general // TODO: use guava caches etc so not unbounded // TODO: do we need to support a transient annotation to exclude certain properties? -// TODO: incorporate the class carpenter for classes not on the classpath. -// TODO: apply class loader logic and an "app context" throughout this code. -// TODO: schema evolution solution when the fingerprints do not line up. // TODO: allow definition of well known types that are left out of the schema. // TODO: generally map Object to '*' all over the place in the schema and make sure use of '*' amd '?' is consistent and documented in generics. // TODO: found a document that states textual descriptors are Symbols. Adjust schema class appropriately. @@ -44,11 +39,19 @@ import javax.annotation.concurrent.ThreadSafe // TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc. // TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact? @ThreadSafe -class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { +class SerializerFactory(val whitelist: ClassWhitelist, cl : ClassLoader) { private val serializersByType = ConcurrentHashMap>() private val serializersByDescriptor = ConcurrentHashMap>() private val customSerializers = CopyOnWriteArrayList>() - private val classCarpenter = ClassCarpenter() + private val classCarpenter = ClassCarpenter(cl) + val classloader : ClassLoader + get() = classCarpenter.classloader + + fun getEvolutionSerializer(typeNotation: TypeNotation, newSerializer: ObjectSerializer) : AMQPSerializer { + return serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { + EvolutionSerializer.make(typeNotation as CompositeType, newSerializer, this) + } + } /** * Look up, and manufacture if necessary, a serializer for the given type. @@ -157,8 +160,9 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { @Throws(NotSerializableException::class) fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer { return serializersByDescriptor[typeDescriptor] ?: { - processSchema(schema) - serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException("Could not find type matching descriptor $typeDescriptor.") + processSchema(schemaAndDescriptor(schema, typeDescriptor)) + serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException( + "Could not find type matching descriptor $typeDescriptor.") }() } @@ -176,15 +180,25 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } } - private fun processSchema(schema: Schema, sentinal: Boolean = false) { + /** + * Iterate over an AMQP schema, for each type ascertain weather it's on ClassPath of [classloader] amd + * if not use the [ClassCarpenter] to generate a class to use in it's place + */ + private fun processSchema(schema: schemaAndDescriptor, sentinel: Boolean = false) { val carpenterSchemas = CarpenterSchemas.newInstance() - for (typeNotation in schema.types) { + for (typeNotation in schema.schema.types) { try { - processSchemaEntry(typeNotation, classCarpenter.classloader) + val serialiser = processSchemaEntry(typeNotation) + + // if we just successfully built a serialiser for the type but the type fingerprint + // doesn't match that of the serialised object then we are dealing with different + // instance of the class, as such we need to build an EvolutionSerialiser + if (serialiser.typeDescriptor != typeNotation.descriptor.name) { + getEvolutionSerializer(typeNotation, serialiser as ObjectSerializer) + } } catch (e: ClassNotFoundException) { - if (sentinal || (typeNotation !is CompositeType)) throw e - typeNotation.carpenterSchema( - classLoaders = listOf(classCarpenter.classloader), carpenterSchemas = carpenterSchemas) + if (sentinel || (typeNotation !is CompositeType)) throw e + typeNotation.carpenterSchema(classloader, carpenterSchemas = carpenterSchemas) } } @@ -195,25 +209,21 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } } - private fun processSchemaEntry(typeNotation: TypeNotation, - cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader) { - when (typeNotation) { - is CompositeType -> processCompositeType(typeNotation, cl) // java.lang.Class (whether a class or interface) + private fun processSchemaEntry(typeNotation: TypeNotation) = when (typeNotation) { + is CompositeType -> processCompositeType(typeNotation) // java.lang.Class (whether a class or interface) is RestrictedType -> processRestrictedType(typeNotation) // Collection / Map, possibly with generics } + + private fun processRestrictedType(typeNotation: RestrictedType): AMQPSerializer { + // TODO: class loader logic, and compare the schema. + val type = typeForName(typeNotation.name, classloader) + return get(null, type) } - private fun processRestrictedType(typeNotation: RestrictedType) { + private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer { // TODO: class loader logic, and compare the schema. - val type = typeForName(typeNotation.name) - get(null, type) - } - - private fun processCompositeType(typeNotation: CompositeType, - cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader) { - // TODO: class loader logic, and compare the schema. - val type = typeForName(typeNotation.name, cl) - get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type) + val type = typeForName(typeNotation.name, classloader) + return get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type) } private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer = serializersByType.computeIfAbsent(type) { @@ -222,7 +232,8 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } else { findCustomSerializer(clazz, declaredType) ?: run { if (type.isArray()) { - whitelisted(type.componentType()) + // Allow Object[] since this can be quite common (i.e. an untyped array) + if(type.componentType() != Object::class.java) whitelisted(type.componentType()) if (clazz.componentType.isPrimitive) PrimArraySerializer.make(type, this) else ArraySerializer.make(type, this) } else if (clazz.kotlin.objectInstance != null) { @@ -256,11 +267,15 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { private fun whitelisted(type: Type) { val clazz = type.asClass()!! - if (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) { + if (isNotWhitelisted(clazz)) { throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.") } } + // Ignore SimpleFieldAccess as we add it to anything we build in the carpenter. + internal fun isNotWhitelisted(clazz: Class<*>): Boolean = clazz == SimpleFieldAccess::class.java || + (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) + // Recursively check the class, interfaces and superclasses for our annotation. internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean { return type.isAnnotationPresent(CordaSerializable::class.java) || @@ -270,7 +285,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer { val rawType = declaredType.rawType as Class<*> - rawType.checkNotUnorderedHashMap() + rawType.checkNotUnsupportedHashMap() return MapSerializer(declaredType, this) } @@ -319,14 +334,13 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } is ParameterizedType -> "${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>" is GenericArrayType -> "${nameForType(type.genericComponentType)}[]" + is WildcardType -> "Any" else -> throw NotSerializableException("Unable to render type $type to a string.") } - private fun typeForName( - name: String, - cl: ClassLoader = DeserializedParameterizedType::class.java.classLoader): Type { + private fun typeForName(name: String, classloader: ClassLoader): Type { return if (name.endsWith("[]")) { - val elementType = typeForName(name.substring(0, name.lastIndex - 1)) + val elementType = typeForName(name.substring(0, name.lastIndex - 1), classloader) if (elementType is ParameterizedType || elementType is GenericArrayType) { DeserializedGenericArrayType(elementType) } else if (elementType is Class<*>) { @@ -349,7 +363,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { else -> throw NotSerializableException("Not able to deserialize array type: $name") } } else { - DeserializedParameterizedType.make(name, cl) + DeserializedParameterizedType.make(name, classloader) } } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt index 22b4a2da6e..dfc587a1dd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt @@ -10,7 +10,7 @@ import java.lang.reflect.Type */ class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer { override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" - private val interfaces = interfacesForSerialization(type) + private val interfaces = interfacesForSerialization(type, factory) private fun generateProvides(): List = interfaces.map { it.typeName } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ClassSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ClassSerializer.kt new file mode 100644 index 0000000000..7c399d5a68 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ClassSerializer.kt @@ -0,0 +1,15 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory + +/** + * A serializer for [Class] that uses [ClassProxy] proxy object to write out + */ +class ClassSerializer(factory: SerializerFactory) : CustomSerializer.Proxy, ClassSerializer.ClassProxy>(Class::class.java, ClassProxy::class.java, factory) { + override fun toProxy(obj: Class<*>): ClassProxy = ClassProxy(obj.name) + + override fun fromProxy(proxy: ClassProxy): Class<*> = Class.forName(proxy.className, true, factory.classloader) + + data class ClassProxy(val className: String) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/DurationSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/DurationSerializer.kt new file mode 100644 index 0000000000..27ef747e79 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/DurationSerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.Duration + +/** + * A serializer for [Duration] that uses a proxy object to write out the seconds and the nanos. + */ +class DurationSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Duration::class.java, DurationProxy::class.java, factory) { + override fun toProxy(obj: Duration): DurationProxy = DurationProxy(obj.seconds, obj.nano) + + override fun fromProxy(proxy: DurationProxy): Duration = Duration.ofSeconds(proxy.seconds, proxy.nanos.toLong()) + + data class DurationProxy(val seconds: Long, val nanos: Int) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/InstantSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/InstantSerializer.kt index 2690d2b6fa..9e3c10d5ec 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/InstantSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/InstantSerializer.kt @@ -8,8 +8,6 @@ import java.time.Instant * A serializer for [Instant] that uses a proxy object to write out the seconds since the epoch and the nanos. */ class InstantSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Instant::class.java, InstantProxy::class.java, factory) { - override val additionalSerializers: Iterable> = emptyList() - override fun toProxy(obj: Instant): InstantProxy = InstantProxy(obj.epochSecond, obj.nano) override fun fromProxy(proxy: InstantProxy): Instant = Instant.ofEpochSecond(proxy.epochSeconds, proxy.nanos.toLong()) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalDateSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalDateSerializer.kt new file mode 100644 index 0000000000..7222924e86 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalDateSerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.LocalDate + +/** + * A serializer for [LocalDate] that uses a proxy object to write out the year, month and day. + */ +class LocalDateSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(LocalDate::class.java, LocalDateProxy::class.java, factory) { + override fun toProxy(obj: LocalDate): LocalDateProxy = LocalDateProxy(obj.year, obj.monthValue.toByte(), obj.dayOfMonth.toByte()) + + override fun fromProxy(proxy: LocalDateProxy): LocalDate = LocalDate.of(proxy.year, proxy.month.toInt(), proxy.day.toInt()) + + data class LocalDateProxy(val year: Int, val month: Byte, val day: Byte) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalDateTimeSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalDateTimeSerializer.kt new file mode 100644 index 0000000000..bfe68ceaa4 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalDateTimeSerializer.kt @@ -0,0 +1,20 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime + +/** + * A serializer for [LocalDateTime] that uses a proxy object to write out the date and time. + */ +class LocalDateTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(LocalDateTime::class.java, LocalDateTimeProxy::class.java, factory) { + override val additionalSerializers: Iterable> = listOf(LocalDateSerializer(factory), LocalTimeSerializer(factory)) + + override fun toProxy(obj: LocalDateTime): LocalDateTimeProxy = LocalDateTimeProxy(obj.toLocalDate(), obj.toLocalTime()) + + override fun fromProxy(proxy: LocalDateTimeProxy): LocalDateTime = LocalDateTime.of(proxy.date, proxy.time) + + data class LocalDateTimeProxy(val date: LocalDate, val time: LocalTime) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalTimeSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalTimeSerializer.kt new file mode 100644 index 0000000000..a355c44a71 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/LocalTimeSerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.LocalTime + +/** + * A serializer for [LocalTime] that uses a proxy object to write out the hours, minutes, seconds and the nanos. + */ +class LocalTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(LocalTime::class.java, LocalTimeProxy::class.java, factory) { + override fun toProxy(obj: LocalTime): LocalTimeProxy = LocalTimeProxy(obj.hour.toByte(), obj.minute.toByte(), obj.second.toByte(), obj.nano) + + override fun fromProxy(proxy: LocalTimeProxy): LocalTime = LocalTime.of(proxy.hour.toInt(), proxy.minute.toInt(), proxy.second.toInt(), proxy.nano) + + data class LocalTimeProxy(val hour: Byte, val minute: Byte, val second: Byte, val nano: Int) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/MonthDaySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/MonthDaySerializer.kt new file mode 100644 index 0000000000..e658ccf9a6 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/MonthDaySerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [MonthDay] that uses a proxy object to write out the integer form. + */ +class MonthDaySerializer(factory: SerializerFactory) : CustomSerializer.Proxy(MonthDay::class.java, MonthDayProxy::class.java, factory) { + override fun toProxy(obj: MonthDay): MonthDayProxy = MonthDayProxy(obj.monthValue.toByte(), obj.dayOfMonth.toByte()) + + override fun fromProxy(proxy: MonthDayProxy): MonthDay = MonthDay.of(proxy.month.toInt(), proxy.day.toInt()) + + data class MonthDayProxy(val month: Byte, val day: Byte) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OffsetDateTimeSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OffsetDateTimeSerializer.kt new file mode 100644 index 0000000000..6000cb68cc --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OffsetDateTimeSerializer.kt @@ -0,0 +1,18 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [OffsetDateTime] that uses a proxy object to write out the date and zone offset. + */ +class OffsetDateTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(OffsetDateTime::class.java, OffsetDateTimeProxy::class.java, factory) { + override val additionalSerializers: Iterable> = listOf(LocalDateTimeSerializer(factory), ZoneIdSerializer(factory)) + + override fun toProxy(obj: OffsetDateTime): OffsetDateTimeProxy = OffsetDateTimeProxy(obj.toLocalDateTime(), obj.offset) + + override fun fromProxy(proxy: OffsetDateTimeProxy): OffsetDateTime = OffsetDateTime.of(proxy.dateTime, proxy.offset) + + data class OffsetDateTimeProxy(val dateTime: LocalDateTime, val offset: ZoneOffset) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OffsetTimeSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OffsetTimeSerializer.kt new file mode 100644 index 0000000000..ec5d25e9a1 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OffsetTimeSerializer.kt @@ -0,0 +1,18 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [OffsetTime] that uses a proxy object to write out the time and zone offset. + */ +class OffsetTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(OffsetTime::class.java, OffsetTimeProxy::class.java, factory) { + override val additionalSerializers: Iterable> = listOf(LocalTimeSerializer(factory), ZoneIdSerializer(factory)) + + override fun toProxy(obj: OffsetTime): OffsetTimeProxy = OffsetTimeProxy(obj.toLocalTime(), obj.offset) + + override fun fromProxy(proxy: OffsetTimeProxy): OffsetTime = OffsetTime.of(proxy.time, proxy.offset) + + data class OffsetTimeProxy(val time: LocalTime, val offset: ZoneOffset) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OpaqueBytesSubSequenceSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OpaqueBytesSubSequenceSerializer.kt new file mode 100644 index 0000000000..71b683da6f --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/OpaqueBytesSubSequenceSerializer.kt @@ -0,0 +1,20 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.OpaqueBytesSubSequence +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory + +/** + * A serializer for [OpaqueBytesSubSequence] that uses a proxy object to write out only content included into sequence + * to save on network bandwidth + * Uses [OpaqueBytes] as a proxy + */ +class OpaqueBytesSubSequenceSerializer(factory: SerializerFactory) : + CustomSerializer.Proxy(OpaqueBytesSubSequence::class.java, OpaqueBytes::class.java, factory) { + override val additionalSerializers: Iterable> = emptyList() + + override fun toProxy(obj: OpaqueBytesSubSequence): OpaqueBytes = obj.copy() as OpaqueBytes + + override fun fromProxy(proxy: OpaqueBytes): OpaqueBytesSubSequence = OpaqueBytesSubSequence(proxy.bytes, proxy.offset, proxy.size) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PartyAndCertificateSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PartyAndCertificateSerializer.kt new file mode 100644 index 0000000000..8bdde643d7 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PartyAndCertificateSerializer.kt @@ -0,0 +1,30 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.core.crypto.Crypto +import net.corda.core.identity.PartyAndCertificate +import net.corda.nodeapi.internal.serialization.amqp.* +import java.io.ByteArrayInputStream +import java.io.NotSerializableException +import java.security.cert.CertPath +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory + +/** + * A serializer that writes out a party and certificate in encoded format. + */ +class PartyAndCertificateSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(PartyAndCertificate::class.java, PartyAndCertificateProxy::class.java, factory) { + override fun toProxy(obj: PartyAndCertificate): PartyAndCertificateProxy = PartyAndCertificateProxy(obj.certPath.type, obj.certPath.encoded) + + override fun fromProxy(proxy: PartyAndCertificateProxy): PartyAndCertificate { + try { + val cf = CertificateFactory.getInstance(proxy.type) + return PartyAndCertificate(cf.generateCertPath(ByteArrayInputStream(proxy.encoded))) + } catch (ce: CertificateException) { + val nse = NotSerializableException("java.security.cert.CertPath: " + type) + nse.initCause(ce) + throw nse + } + } + + data class PartyAndCertificateProxy(val type: String, val encoded: ByteArray) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PeriodSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PeriodSerializer.kt new file mode 100644 index 0000000000..c850489d0c --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PeriodSerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [Period] that uses a proxy object to write out the integer form. + */ +class PeriodSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Period::class.java, PeriodProxy::class.java, factory) { + override fun toProxy(obj: Period): PeriodProxy = PeriodProxy(obj.years, obj.months, obj.days) + + override fun fromProxy(proxy: PeriodProxy): Period = Period.of(proxy.years, proxy.months, proxy.days) + + data class PeriodProxy(val years: Int, val months: Int, val days: Int) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PublicKeySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PublicKeySerializer.kt index 768a35766a..95a5907cc6 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PublicKeySerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/PublicKeySerializer.kt @@ -10,8 +10,6 @@ import java.security.PublicKey * A serializer that writes out a public key in X.509 format. */ object PublicKeySerializer : CustomSerializer.Implements(PublicKey::class.java) { - override val additionalSerializers: Iterable> = emptyList() - override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList()))) override fun writeDescribedObject(obj: PublicKey, data: Data, type: Type, output: SerializationOutput) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt index ff7bf77740..c87d1cef54 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ThrowableSerializer.kt @@ -19,7 +19,7 @@ class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) { - override val additionalSerializers: Iterable> = emptyList() - override fun toProxy(obj: StackTraceElement): StackTraceElementProxy = StackTraceElementProxy(obj.className, obj.methodName, obj.fileName, obj.lineNumber) override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/X500NameSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/X500NameSerializer.kt index 399d204592..0138d28dd0 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/X500NameSerializer.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/X500NameSerializer.kt @@ -10,8 +10,6 @@ import java.lang.reflect.Type * Custom serializer for X500 names that utilizes their ASN.1 encoding on the wire. */ object X500NameSerializer : CustomSerializer.Implements(X500Name::class.java) { - override val additionalSerializers: Iterable> = emptyList() - override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList()))) override fun writeDescribedObject(obj: X500Name, data: Data, type: Type, output: SerializationOutput) { diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/X509CertificateHolderSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/X509CertificateHolderSerializer.kt new file mode 100644 index 0000000000..13ca64dd75 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/X509CertificateHolderSerializer.kt @@ -0,0 +1,23 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.core.crypto.Crypto +import net.corda.nodeapi.internal.serialization.amqp.* +import org.apache.qpid.proton.codec.Data +import org.bouncycastle.cert.X509CertificateHolder +import java.lang.reflect.Type + +/** + * A serializer that writes out a certificate in X.509 format. + */ +object X509CertificateHolderSerializer : CustomSerializer.Implements(X509CertificateHolder::class.java) { + override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList()))) + + override fun writeDescribedObject(obj: X509CertificateHolder, data: Data, type: Type, output: SerializationOutput) { + output.writeObject(obj.encoded, data, clazz) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): X509CertificateHolder { + val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray + return X509CertificateHolder(bits) + } +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/YearMonthSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/YearMonthSerializer.kt new file mode 100644 index 0000000000..1b97e1d131 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/YearMonthSerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [YearMonth] that uses a proxy object to write out the integer form. + */ +class YearMonthSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(YearMonth::class.java, YearMonthProxy::class.java, factory) { + override fun toProxy(obj: YearMonth): YearMonthProxy = YearMonthProxy(obj.year, obj.monthValue.toByte()) + + override fun fromProxy(proxy: YearMonthProxy): YearMonth = YearMonth.of(proxy.year, proxy.month.toInt()) + + data class YearMonthProxy(val year: Int, val month: Byte) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/YearSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/YearSerializer.kt new file mode 100644 index 0000000000..f96c5ed7e9 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/YearSerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [Year] that uses a proxy object to write out the integer form. + */ +class YearSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Year::class.java, YearProxy::class.java, factory) { + override fun toProxy(obj: Year): YearProxy = YearProxy(obj.value) + + override fun fromProxy(proxy: YearProxy): Year = Year.of(proxy.year) + + data class YearProxy(val year: Int) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ZoneIdSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ZoneIdSerializer.kt new file mode 100644 index 0000000000..87ab278dda --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ZoneIdSerializer.kt @@ -0,0 +1,16 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [ZoneId] that uses a proxy object to write out the string form. + */ +class ZoneIdSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(ZoneId::class.java, ZoneIdProxy::class.java, factory) { + override fun toProxy(obj: ZoneId): ZoneIdProxy = ZoneIdProxy(obj.id) + + override fun fromProxy(proxy: ZoneIdProxy): ZoneId = ZoneId.of(proxy.id) + + data class ZoneIdProxy(val id: String) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ZonedDateTimeSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ZonedDateTimeSerializer.kt new file mode 100644 index 0000000000..86a5f0b3f4 --- /dev/null +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/custom/ZonedDateTimeSerializer.kt @@ -0,0 +1,26 @@ +package net.corda.nodeapi.internal.serialization.amqp.custom + +import net.corda.nodeapi.internal.serialization.amqp.CustomSerializer +import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory +import java.time.* + +/** + * A serializer for [ZonedDateTime] that uses a proxy object to write out the date, time, offset and zone. + */ +class ZonedDateTimeSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(ZonedDateTime::class.java, ZonedDateTimeProxy::class.java, factory) { + // Java deserialization of `ZonedDateTime` uses a private method. We will resolve this somewhat statically + // so that any change to internals of `ZonedDateTime` is detected early. + companion object { + val ofLenient = ZonedDateTime::class.java.getDeclaredMethod("ofLenient", LocalDateTime::class.java, ZoneOffset::class.java, ZoneId::class.java) + init { + ofLenient.isAccessible = true + } + } + override val additionalSerializers: Iterable> = listOf(LocalDateTimeSerializer(factory), ZoneIdSerializer(factory)) + + override fun toProxy(obj: ZonedDateTime): ZonedDateTimeProxy = ZonedDateTimeProxy(obj.toLocalDateTime(), obj.offset, obj.zone) + + override fun fromProxy(proxy: ZonedDateTimeProxy): ZonedDateTime = ofLenient.invoke(null, proxy.dateTime, proxy.offset, proxy.zone) as ZonedDateTime + + data class ZonedDateTimeProxy(val dateTime: LocalDateTime, val offset: ZoneOffset, val zone: ZoneId) +} \ No newline at end of file diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt index dbe8bb254d..eb1f21f54a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/AMQPSchemaExtensions.kt @@ -4,13 +4,11 @@ import net.corda.nodeapi.internal.serialization.amqp.CompositeType import net.corda.nodeapi.internal.serialization.amqp.Field as AMQPField import net.corda.nodeapi.internal.serialization.amqp.Schema as AMQPSchema -fun AMQPSchema.carpenterSchema( - loaders: List = listOf(ClassLoader.getSystemClassLoader())) - : CarpenterSchemas { +fun AMQPSchema.carpenterSchema(classloader: ClassLoader) : CarpenterSchemas { val rtn = CarpenterSchemas.newInstance() types.filterIsInstance().forEach { - it.carpenterSchema(classLoaders = loaders, carpenterSchemas = rtn) + it.carpenterSchema(classloader, carpenterSchemas = rtn) } return rtn @@ -19,10 +17,9 @@ fun AMQPSchema.carpenterSchema( /** * if we can load the class then we MUST know about all of it's composite elements */ -private fun CompositeType.validatePropertyTypes( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader())) { +private fun CompositeType.validatePropertyTypes(classloader: ClassLoader) { fields.forEach { - if (!it.validateType(classLoaders)) throw UncarpentableException(name, it.name, it.type) + if (!it.validateType(classloader)) throw UncarpentableException(name, it.name, it.type) } } @@ -34,24 +31,21 @@ fun AMQPField.typeAsString() = if (type == "*") requires[0] else type * b) add the class to the dependency tree in [carpenterSchemas] if it cannot be instantiated * at this time * - * @param classLoaders list of classLoaders, defaulting toe the system class loader, that might - * be used to load objects + * @param classloader the class loader provided dby the [SerializationContext] * @param carpenterSchemas structure that holds the dependency tree and list of classes that * need constructing * @param force by default a schema is not added to [carpenterSchemas] if it already exists * on the class path. For testing purposes schema generation can be forced */ -fun CompositeType.carpenterSchema( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader()), - carpenterSchemas: CarpenterSchemas, - force: Boolean = false) { - if (classLoaders.exists(name)) { - validatePropertyTypes(classLoaders) +fun CompositeType.carpenterSchema(classloader: ClassLoader, + carpenterSchemas: CarpenterSchemas, + force: Boolean = false) { + if (classloader.exists(name)) { + validatePropertyTypes(classloader) if (!force) return } val providesList = mutableListOf>() - var isInterface = false var isCreatable = true @@ -62,7 +56,7 @@ fun CompositeType.carpenterSchema( } try { - providesList.add(classLoaders.loadIfExists(it)) + providesList.add(classloader.loadClass(it)) } catch (e: ClassNotFoundException) { carpenterSchemas.addDepPair(this, name, it) isCreatable = false @@ -73,7 +67,7 @@ fun CompositeType.carpenterSchema( fields.forEach { try { - m[it.name] = FieldFactory.newInstance(it.mandatory, it.name, it.getTypeAsClass(classLoaders)) + m[it.name] = FieldFactory.newInstance(it.mandatory, it.name, it.getTypeAsClass(classloader)) } catch (e: ClassNotFoundException) { carpenterSchemas.addDepPair(this, name, it.typeAsString()) isCreatable = false @@ -110,33 +104,18 @@ val typeStrToType: Map, Class> = mapOf( Pair("byte", false) to Byte::class.javaObjectType ) -fun AMQPField.getTypeAsClass( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader()) -) = typeStrToType[Pair(type, mandatory)] ?: when (type) { +fun AMQPField.getTypeAsClass(classloader: ClassLoader) = typeStrToType[Pair(type, mandatory)] ?: when (type) { "string" -> String::class.java - "*" -> classLoaders.loadIfExists(requires[0]) - else -> classLoaders.loadIfExists(type) + "*" -> classloader.loadClass(requires[0]) + else -> classloader.loadClass(type) } -fun AMQPField.validateType( - classLoaders: List = listOf(ClassLoader.getSystemClassLoader()) -) = when (type) { +fun AMQPField.validateType(classloader: ClassLoader) = when (type) { "byte", "int", "string", "short", "long", "char", "boolean", "double", "float" -> true - "*" -> classLoaders.exists(requires[0]) - else -> classLoaders.exists(type) + "*" -> classloader.exists(requires[0]) + else -> classloader.exists(type) } -private fun List.exists(clazz: String) = this.find { - try { it.loadClass(clazz); true } catch (e: ClassNotFoundException) { false } -} != null +private fun ClassLoader.exists(clazz: String) = run { + try { this.loadClass(clazz); true } catch (e: ClassNotFoundException) { false } } -private fun List.loadIfExists(clazz: String): Class<*> { - this.forEach { - try { - return it.loadClass(clazz) - } catch (e: ClassNotFoundException) { - return@forEach - } - } - throw ClassNotFoundException(clazz) -} diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt index 21641a0ae1..f62f5ca1b1 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenter.kt @@ -1,8 +1,10 @@ package net.corda.nodeapi.internal.serialization.carpenter +import net.corda.core.serialization.CordaSerializable import org.objectweb.asm.ClassWriter import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* +import org.objectweb.asm.Type import java.lang.Character.isJavaIdentifierPart import java.lang.Character.isJavaIdentifierStart @@ -18,7 +20,8 @@ interface SimpleFieldAccess { operator fun get(name: String): Any? } -class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoader) { +class CarpenterClassLoader (parentClassLoader: ClassLoader = Thread.currentThread().contextClassLoader) : + ClassLoader(parentClassLoader) { fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size) } @@ -66,14 +69,14 @@ class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoad * * Equals/hashCode methods are not yet supported. */ -class ClassCarpenter { +class ClassCarpenter(cl: ClassLoader = Thread.currentThread().contextClassLoader) { // TODO: Generics. // TODO: Sandbox the generated code when a security manager is in use. // TODO: Generate equals/hashCode. // TODO: Support annotations. // TODO: isFoo getter patterns for booleans (this is what Kotlin generates) - val classloader = CarpenterClassLoader() + val classloader = CarpenterClassLoader(cl) private val _loaded = HashMap>() private val String.jvm: String get() = replace(".", "/") @@ -118,6 +121,7 @@ class ClassCarpenter { with(cw) { visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, "java/lang/Object", interfaces) + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() generateAbstractGetters(schema) @@ -135,6 +139,7 @@ class ClassCarpenter { with(cw) { visit(V1_8, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces.toTypedArray()) + visitAnnotation(Type.getDescriptor(CordaSerializable::class.java), true).visitEnd() generateFields(schema) generateConstructor(schema) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt index 77e866fb32..fb0d1f7001 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MetaCarpenter.kt @@ -73,9 +73,7 @@ abstract class MetaCarpenterBase (val schemas : CarpenterSchemas, val cc : Class // we're out of blockers so we can now create the type if (schemas.dependencies[dependent]?.second?.isEmpty() ?: false) { (schemas.dependencies.remove (dependent)?.first as CompositeType).carpenterSchema ( - classLoaders = listOf ( - ClassLoader.getSystemClassLoader(), - cc.classloader), + classloader = cc.classloader, carpenterSchemas = schemas) } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java similarity index 94% rename from node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java rename to node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java index afb558ebdb..171b9cbe72 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java +++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp; +import net.corda.nodeapi.internal.serialization.AllWhitelist; import net.corda.core.serialization.SerializedBytes; import org.apache.qpid.proton.codec.DecoderImpl; import org.apache.qpid.proton.codec.EncoderImpl; @@ -167,8 +168,9 @@ public class JavaSerializationOutputTests { } private Object serdes(Object obj) throws NotSerializableException { - SerializerFactory factory = new SerializerFactory(); - SerializationOutput ser = new SerializationOutput(factory); + SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); + SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader()); + SerializationOutput ser = new SerializationOutput(factory1); SerializedBytes bytes = ser.serialize(obj); DecoderImpl decoder = new DecoderImpl(); @@ -186,13 +188,13 @@ public class JavaSerializationOutputTests { Envelope result = (Envelope) decoder.readObject(); assertTrue(result != null); - DeserializationInput des = new DeserializationInput(); + DeserializationInput des = new DeserializationInput(factory2); Object desObj = des.deserialize(bytes, Object.class); assertTrue(Objects.deepEquals(obj, desObj)); // Now repeat with a re-used factory - SerializationOutput ser2 = new SerializationOutput(factory); - DeserializationInput des2 = new DeserializationInput(factory); + SerializationOutput ser2 = new SerializationOutput(factory1); + DeserializationInput des2 = new DeserializationInput(factory1); Object desObj2 = des2.deserialize(ser2.serialize(obj), Object.class); assertTrue(Objects.deepEquals(obj, desObj2)); // TODO: check schema is as expected diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt index 284127a29a..3773a0dee6 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/AttachmentClassLoaderTests.kt @@ -6,12 +6,17 @@ import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.declaredField import net.corda.core.node.ServiceHub import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* import net.corda.core.serialization.SerializationDefaults.P2P_CONTEXT import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ByteSequence +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.loggerFor +import net.corda.nodeapi.internal.serialization.AMQP_ENABLED import net.corda.nodeapi.internal.serialization.SerializeAsTokenContextImpl import net.corda.nodeapi.internal.serialization.WireTransactionSerializer import net.corda.nodeapi.internal.serialization.withTokenContext @@ -66,9 +71,6 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { // Always accepts. } - // The "empty contract" - override val legalContractReference: SecureHash = SecureHash.sha256("") - fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder { val state = State(magicNumber) return TransactionBuilder(notary).withItems(state, Command(Commands.Create(), owner.party.owningKey)) @@ -99,7 +101,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, child) val contract = contractClass.newInstance() as Contract - assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference) + assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.declaredField("legalContractReference").value) } fun fakeAttachment(filepath: String, content: String): ByteArray { @@ -190,7 +192,7 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, cl) val contract = contractClass.newInstance() as Contract assertEquals(cl, contract.javaClass.classLoader) - assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.legalContractReference) + assertEquals(SecureHash.sha256("https://anotherdummy.org"), contract.declaredField("legalContractReference").value) } @@ -266,6 +268,32 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { assertNotNull(state3) } + @Test + fun `test serialization of SecureHash`() { + val secureHash = SecureHash.randomSHA256() + val bytes = secureHash.serialize() + val copiedSecuredHash = bytes.deserialize() + + assertEquals(secureHash, copiedSecuredHash) + } + + @Test + fun `test serialization of OpaqueBytes`() { + val opaqueBytes = OpaqueBytes("0123456789".toByteArray()) + val bytes = opaqueBytes.serialize() + val copiedOpaqueBytes = bytes.deserialize() + + assertEquals(opaqueBytes, copiedOpaqueBytes) + } + + @Test + fun `test serialization of sub-sequence OpaqueBytes`() { + val bytesSequence = ByteSequence.of("0123456789".toByteArray(), 3 ,2) + val bytes = bytesSequence.serialize() + val copiedBytesSequence = bytes.deserialize() + + assertEquals(bytesSequence, copiedBytesSequence) + } @Test fun `test serialization of WireTransaction with statically loaded contract`() { @@ -304,7 +332,8 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { } @Test - fun `test deserialize of WireTransaction where contract cannot be found`() { + // Kryo verifies/loads attachments on deserialization, whereas AMQP currently does not + fun `test deserialize of WireTransaction where contract cannot be found`() = kryoSpecific { val child = ClassLoaderForTests() val contractClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, child) val contract = contractClass.newInstance() as DummyContractBackdoor @@ -324,8 +353,19 @@ class AttachmentClassLoaderTests : TestDependencyInjectionBase() { // use empty attachmentStorage val e = assertFailsWith(MissingAttachmentsException::class) { - bytes.deserialize(context = P2P_CONTEXT.withAttachmentStorage(MockAttachmentStorage())) + val mockAttStorage = MockAttachmentStorage() + bytes.deserialize(context = P2P_CONTEXT.withAttachmentStorage(mockAttStorage)) + + if(mockAttStorage.openAttachment(attachmentRef) == null) { + throw MissingAttachmentsException(listOf(attachmentRef)) + } } assertEquals(attachmentRef, e.ids.single()) } -} + + private fun kryoSpecific(function: () -> Unit) = if(!AMQP_ENABLED) { + function() + } else { + loggerFor().info("Ignoring Kryo specific test") + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt index 88ac9f4049..11eb91de40 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/CordaClassResolverTests.kt @@ -87,73 +87,73 @@ class CordaClassResolverTests { } - val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P) - val allButBlacklistedContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P) + private val emptyWhitelistContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, EmptyWhitelist, emptyMap(), true, SerializationContext.UseCase.P2P) + private val allButBlacklistedContext: SerializationContext = SerializationContextImpl(KryoHeaderV0_1, this.javaClass.classLoader, AllButBlacklisted, emptyMap(), true, SerializationContext.UseCase.P2P) @Test fun `Annotation on enum works for specialised entries`() { // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. @Suppress("UNSUPPORTED_FEATURE") - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Foo.Bar::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(Foo.Bar::class.java) } @Test fun `Annotation on array element works`() { val values = arrayOf(Element()) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(values.javaClass) + CordaClassResolver(emptyWhitelistContext).getRegistration(values.javaClass) } @Test fun `Annotation not needed on abstract class`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(AbstractClass::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(AbstractClass::class.java) } @Test fun `Annotation not needed on interface`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Interface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(Interface::class.java) } @Test fun `Calling register method on modified Kryo does not consult the whitelist`() { - val kryo = CordaKryo(CordaClassResolver(factory, emptyWhitelistContext)) + val kryo = CordaKryo(CordaClassResolver(emptyWhitelistContext)) kryo.register(NotSerializable::class.java) } @Test(expected = KryoException::class) fun `Calling register method on unmodified Kryo does consult the whitelist`() { - val kryo = Kryo(CordaClassResolver(factory, emptyWhitelistContext), MapReferenceResolver()) + val kryo = Kryo(CordaClassResolver(emptyWhitelistContext), MapReferenceResolver()) kryo.register(NotSerializable::class.java) } @Test(expected = KryoException::class) fun `Annotation is needed without whitelisting`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(NotSerializable::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(NotSerializable::class.java) } @Test fun `Annotation is not needed with whitelisting`() { - val resolver = CordaClassResolver(factory, emptyWhitelistContext.withWhitelisted(NotSerializable::class.java)) + val resolver = CordaClassResolver(emptyWhitelistContext.withWhitelisted(NotSerializable::class.java)) resolver.getRegistration(NotSerializable::class.java) } @Test fun `Annotation not needed on Object`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Object::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(Object::class.java) } @Test fun `Annotation not needed on primitive`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(Integer.TYPE) + CordaClassResolver(emptyWhitelistContext).getRegistration(Integer.TYPE) } @Test(expected = KryoException::class) fun `Annotation does not work for custom serializable`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(CustomSerializable::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(CustomSerializable::class.java) } @Test(expected = KryoException::class) fun `Annotation does not work in conjunction with Kryo annotation`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(DefaultSerializable::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(DefaultSerializable::class.java) } private fun importJar(storage: AttachmentStorage) = AttachmentClassLoaderTests.ISOLATED_CONTRACTS_JAR_PATH.openStream().use { storage.importAttachment(it) } @@ -164,23 +164,23 @@ class CordaClassResolverTests { val attachmentHash = importJar(storage) val classLoader = AttachmentsClassLoader(arrayOf(attachmentHash).map { storage.openAttachment(it)!! }) val attachedClass = Class.forName("net.corda.contracts.isolated.AnotherDummyContract", true, classLoader) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(attachedClass) + CordaClassResolver(emptyWhitelistContext).getRegistration(attachedClass) } @Test fun `Annotation is inherited from interfaces`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SerializableViaInterface::class.java) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SerializableViaSubInterface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaInterface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaSubInterface::class.java) } @Test fun `Annotation is inherited from superclass`() { - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SubElement::class.java) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SubSubElement::class.java) - CordaClassResolver(factory, emptyWhitelistContext).getRegistration(SerializableViaSuperSubInterface::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SubElement::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SubSubElement::class.java) + CordaClassResolver(emptyWhitelistContext).getRegistration(SerializableViaSuperSubInterface::class.java) } - // Blacklist tests. + // Blacklist tests. Note: leave the variable public or else expected messages do not work correctly @get:Rule val expectedEx = ExpectedException.none()!! @@ -188,7 +188,7 @@ class CordaClassResolverTests { fun `Check blacklisted class`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("Class java.util.HashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // HashSet is blacklisted. resolver.getRegistration(HashSet::class.java) } @@ -198,7 +198,7 @@ class CordaClassResolverTests { fun `Check blacklisted subclass`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$SubHashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // SubHashSet extends the blacklisted HashSet. resolver.getRegistration(SubHashSet::class.java) } @@ -208,35 +208,35 @@ class CordaClassResolverTests { fun `Check blacklisted subsubclass`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$SubSubHashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // SubSubHashSet extends SubHashSet, which extends the blacklisted HashSet. resolver.getRegistration(SubSubHashSet::class.java) } - class ConnectionImpl(val connection: Connection) : Connection by connection + class ConnectionImpl(private val connection: Connection) : Connection by connection @Test fun `Check blacklisted interface impl`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$ConnectionImpl is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // ConnectionImpl implements blacklisted Connection. resolver.getRegistration(ConnectionImpl::class.java) } interface SubConnection : Connection - class SubConnectionImpl(val subConnection: SubConnection) : SubConnection by subConnection + class SubConnectionImpl(private val subConnection: SubConnection) : SubConnection by subConnection @Test fun `Check blacklisted super-interface impl`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$SubConnectionImpl is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // SubConnectionImpl implements SubConnection, which extends the blacklisted Connection. resolver.getRegistration(SubConnectionImpl::class.java) } @Test fun `Check forcibly allowed`() { - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // LinkedHashSet is allowed for serialization. resolver.getRegistration(LinkedHashSet::class.java) } @@ -247,7 +247,7 @@ class CordaClassResolverTests { fun `Check blacklist precedes CordaSerializable`() { expectedEx.expect(IllegalStateException::class.java) expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.nodeapi.internal.serialization.CordaClassResolverTests\$CordaSerializableHashSet is blacklisted, so it cannot be used in serialization.") - val resolver = CordaClassResolver(factory, allButBlacklistedContext) + val resolver = CordaClassResolver(allButBlacklistedContext) // CordaSerializableHashSet is @CordaSerializable, but extends the blacklisted HashSet. resolver.getRegistration(CordaSerializableHashSet::class.java) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt index 66918ac812..24ed6f19cb 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/KryoTests.kt @@ -5,7 +5,9 @@ import com.esotericsoftware.kryo.KryoSerializable import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.google.common.primitives.Ints +import net.corda.core.contracts.PrivacySalt import net.corda.core.crypto.* +import net.corda.core.internal.FetchDataFlow import net.corda.core.serialization.* import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.sequence @@ -16,21 +18,27 @@ import net.corda.testing.TestDependencyInjectionBase import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.InputStream import java.time.Instant import kotlin.test.assertEquals +import kotlin.test.assertNotNull import kotlin.test.assertTrue class KryoTests : TestDependencyInjectionBase() { private lateinit var factory: SerializationFactory private lateinit var context: SerializationContext + @get:Rule + val expectedEx: ExpectedException = ExpectedException.none() + @Before fun setup() { - factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, @@ -157,6 +165,26 @@ class KryoTests : TestDependencyInjectionBase() { override fun toString(): String = "Cyclic($value)" } + @Test + fun `serialize - deserialize PrivacySalt`() { + val expected = PrivacySalt(byteArrayOf( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32 + )) + val serializedBytes = expected.serialize(factory, context) + val actual = serializedBytes.deserialize(factory, context) + assertEquals(expected, actual) + } + + @Test + fun `all-zero PrivacySalt not allowed`() { + expectedEx.expect(IllegalArgumentException::class.java) + expectedEx.expectMessage("Privacy salt should not be all zeros.") + PrivacySalt(ByteArray(32)) + } + @CordaSerializable private object TestSingleton @@ -200,7 +228,7 @@ class KryoTests : TestDependencyInjectionBase() { } } Tmp() - val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, @@ -209,4 +237,42 @@ class KryoTests : TestDependencyInjectionBase() { SerializationContext.UseCase.P2P) pt.serialize(factory, context) } -} + + @Test + fun `serialize - deserialize Exception with suppressed`() { + val exception = IllegalArgumentException("fooBar") + val toBeSuppressedOnSenderSide = IllegalStateException("bazz1") + exception.addSuppressed(toBeSuppressedOnSenderSide) + val exception2 = exception.serialize(factory, context).deserialize(factory, context) + assertEquals(exception.message, exception2.message) + + assertEquals(1, exception2.suppressed.size) + assertNotNull({ exception2.suppressed.find { it.message == toBeSuppressedOnSenderSide.message }}) + + val toBeSuppressedOnReceiverSide = IllegalStateException("bazz2") + exception2.addSuppressed(toBeSuppressedOnReceiverSide) + assertTrue { exception2.suppressed.contains(toBeSuppressedOnReceiverSide) } + assertEquals(2, exception2.suppressed.size) + } + + @Test + fun `serialize - deserialize Exception no suppressed`() { + val exception = IllegalArgumentException("fooBar") + val exception2 = exception.serialize(factory, context).deserialize(factory, context) + assertEquals(exception.message, exception2.message) + assertEquals(0, exception2.suppressed.size) + + val toBeSuppressedOnReceiverSide = IllegalStateException("bazz2") + exception2.addSuppressed(toBeSuppressedOnReceiverSide) + assertEquals(1, exception2.suppressed.size) + assertTrue { exception2.suppressed.contains(toBeSuppressedOnReceiverSide) } + } + + @Test + fun `serialize - deserialize HashNotFound`() { + val randomHash = SecureHash.randomSHA256() + val exception = FetchDataFlow.HashNotFound(randomHash) + val exception2 = exception.serialize(factory, context).deserialize(factory, context) + assertEquals(randomHash, exception2.requested) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt index 03ab48214d..2c8a337035 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/SerializationTokenTest.kt @@ -4,10 +4,8 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.io.Output import com.nhaarman.mockito_kotlin.mock -import net.corda.core.node.ServiceHub import net.corda.core.serialization.* import net.corda.core.utilities.OpaqueBytes -import net.corda.node.serialization.KryoServerSerializationScheme import net.corda.testing.TestDependencyInjectionBase import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -16,18 +14,13 @@ import java.io.ByteArrayOutputStream class SerializationTokenTest : TestDependencyInjectionBase() { - lateinit var factory: SerializationFactory - lateinit var context: SerializationContext + private lateinit var factory: SerializationFactory + private lateinit var context: SerializationContext @Before fun setup() { - factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } - context = SerializationContextImpl(KryoHeaderV0_1, - javaClass.classLoader, - AllWhitelist, - emptyMap(), - true, - SerializationContext.UseCase.P2P) + factory = SerializationDefaults.SERIALIZATION_FACTORY + context = SerializationDefaults.CHECKPOINT_CONTEXT.withWhitelisted(SingletonSerializationToken::class.java) } // Large tokenizable object so we can tell from the smaller number of serialized bytes it was actually tokenized @@ -42,7 +35,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { override fun equals(other: Any?) = other is LargeTokenizable && other.bytes.size == this.bytes.size } - private fun serializeAsTokenContext(toBeTokenized: Any) = SerializeAsTokenContextImpl(toBeTokenized, factory, context, mock()) + private fun serializeAsTokenContext(toBeTokenized: Any) = SerializeAsTokenContextImpl(toBeTokenized, factory, context, mock()) @Test fun `write token and read tokenizable`() { @@ -97,7 +90,7 @@ class SerializationTokenTest : TestDependencyInjectionBase() { val context = serializeAsTokenContext(tokenizableBefore) val testContext = this.context.withTokenContext(context) - val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(factory, this.context))) + val kryo: Kryo = DefaultKryoCustomizer.customize(CordaKryo(CordaClassResolver(this.context))) val stream = ByteArrayOutputStream() Output(stream).use { it.write(KryoHeaderV0_1.bytes) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt index 19b93b6542..5cc7bbc125 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/AMQPTestUtils.kt @@ -1,10 +1,16 @@ package net.corda.nodeapi.internal.serialization.amqp import org.apache.qpid.proton.codec.Data +import net.corda.nodeapi.internal.serialization.AllWhitelist +import net.corda.nodeapi.internal.serialization.EmptyWhitelist + +fun testDefaultFactory() = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) +fun testDefaultFactoryWithWhitelist() = SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader()) class TestSerializationOutput( private val verbose: Boolean, - serializerFactory: SerializerFactory = SerializerFactory()) : SerializationOutput(serializerFactory) { + serializerFactory: SerializerFactory = testDefaultFactory()) + : SerializationOutput(serializerFactory) { override fun writeSchema(schema: Schema, data: Data) { if (verbose) println(schema) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt index b91fa3fe9e..db59a9c2c8 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeAndReturnEnvelopeTests.kt @@ -1,5 +1,6 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.CordaSerializable import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -18,7 +19,7 @@ class DeserializeAndReturnEnvelopeTests { val a = A(10, "20") - val factory = SerializerFactory() + val factory = testDefaultFactory() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) @@ -34,7 +35,7 @@ class DeserializeAndReturnEnvelopeTests { val b = B(A(10, "20"), 30.0F) - val factory = SerializerFactory() + val factory = testDefaultFactory() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(b)) @@ -43,4 +44,22 @@ class DeserializeAndReturnEnvelopeTests { assertNotEquals(null, obj.envelope.schema.types.find { it.name == classTestName("A") }) assertNotEquals(null, obj.envelope.schema.types.find { it.name == classTestName("B") }) } + + @Test + fun unannotatedInterfaceIsNotInSchema() { + @CordaSerializable + data class Foo(val bar: Int) : Comparable { + override fun compareTo(other: Foo): Int = bar.compareTo(other.bar) + } + + val a = Foo(123) + val factory = testDefaultFactoryWithWhitelist() + fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) + val obj = DeserializationInput(factory).deserializeAndReturnEnvelope(serialise(a)) + + assertTrue(obj.obj is Foo) + assertEquals(1, obj.envelope.schema.types.size) + assertNotEquals(null, obj.envelope.schema.types.find { it.name == classTestName("Foo") }) + assertEquals(null, obj.envelope.schema.types.find { it.name == "java.lang.Comparable<${classTestName("Foo")}>" }) + } } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt new file mode 100644 index 0000000000..162756b06c --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeMapTests.kt @@ -0,0 +1,118 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import org.junit.Test +import java.util.* + +class DeserializeCollectionTests { + companion object { + /** + * If you want to see the schema encoded into the envelope after serialisation change this to true + */ + private const val VERBOSE = false + } + + val sf = testDefaultFactory() + + @Test + fun mapTest() { + data class C(val c: Map) + val c = C (mapOf("A" to 1, "B" to 2)) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test(expected=java.io.NotSerializableException::class) + fun abstractMapFromMapOf() { + data class C(val c: AbstractMap) + val c = C (mapOf("A" to 1, "B" to 2) as AbstractMap) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test(expected=java.io.NotSerializableException::class) + fun abstractMapFromTreeMap() { + data class C(val c: AbstractMap) + val c = C (TreeMap(mapOf("A" to 1, "B" to 2))) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun sortedMapTest() { + data class C(val c: SortedMap) + val c = C(sortedMapOf ("A" to 1, "B" to 2)) + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun navigableMapTest() { + data class C(val c: NavigableMap) + val c = C(TreeMap (mapOf("A" to 1, "B" to 2)).descendingMap()) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test(expected=java.io.NotSerializableException::class) + fun dictionaryTest() { + data class C(val c: Dictionary) + val v : Hashtable = Hashtable() + v.put ("a", 1) + v.put ("b", 2) + val c = C(v) + + // expected to throw + TestSerializationOutput(VERBOSE, sf).serialize(c) + } + + @Test(expected=java.lang.IllegalArgumentException::class) + fun hashtableTest() { + data class C(val c: Hashtable) + val v : Hashtable = Hashtable() + v.put ("a", 1) + v.put ("b", 2) + val c = C(v) + + // expected to throw + TestSerializationOutput(VERBOSE, sf).serialize(c) + } + + @Test(expected=java.lang.IllegalArgumentException::class) + fun hashMapTest() { + data class C(val c : HashMap) + val c = C (HashMap (mapOf("A" to 1, "B" to 2))) + + // expect this to throw + TestSerializationOutput(VERBOSE, sf).serialize(c) + } + + @Test(expected=java.lang.IllegalArgumentException::class) + fun weakHashMapTest() { + data class C(val c : WeakHashMap) + val c = C (WeakHashMap (mapOf("A" to 1, "B" to 2))) + + TestSerializationOutput(VERBOSE, sf).serialize(c) + } + + @Test + fun concreteTreeMapTest() { + data class C(val c: TreeMap) + val c = C(TreeMap (mapOf("A" to 1, "B" to 3))) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } + + @Test + fun concreteLinkedHashMapTest() { + data class C(val c : LinkedHashMap) + val c = C (LinkedHashMap (mapOf("A" to 1, "B" to 2))) + + val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(c) + DeserializationInput(sf).deserialize(serialisedBytes) + } +} diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt index 76933fb370..0a05542ad7 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentrySimpleTypesTest.kt @@ -8,7 +8,7 @@ import net.corda.nodeapi.internal.serialization.carpenter.* // those classes don't exist within the system's Class Loader the deserialiser will be forced to carpent // versions of them up using its own internal class carpenter (each carpenter houses it's own loader). This // replicates the situation where a receiver doesn't have some or all elements of a schema present on it's classpath -class DeserializeNeedingCarpentrySimpleTypesTest { +class DeserializeNeedingCarpentrySimpleTypesTest : AmqpCarpenterBase() { companion object { /** * If you want to see the schema encoded into the envelope after serialisation change this to true @@ -16,12 +16,12 @@ class DeserializeNeedingCarpentrySimpleTypesTest { private const val VERBOSE = false } - val sf = SerializerFactory() - val sf2 = SerializerFactory() + val sf = testDefaultFactory() + val sf2 = testDefaultFactory() @Test fun singleInt() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "int" to NonNullableField(Integer::class.javaPrimitiveType!!) ))) @@ -41,7 +41,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleIntNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "int" to NullableField(Integer::class.java) ))) @@ -57,7 +57,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleIntNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "int" to NullableField(Integer::class.java) ))) @@ -73,7 +73,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleChar() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "char" to NonNullableField(Character::class.javaPrimitiveType!!) ))) @@ -86,7 +86,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleCharNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "char" to NullableField(Character::class.javaObjectType) ))) @@ -99,7 +99,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleCharNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "char" to NullableField(java.lang.Character::class.java) ))) @@ -112,7 +112,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleLong() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "long" to NonNullableField(Long::class.javaPrimitiveType!!) ))) @@ -126,7 +126,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleLongNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "long" to NullableField(Long::class.javaObjectType) ))) @@ -140,7 +140,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleLongNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "long" to NullableField(Long::class.javaObjectType) ))) @@ -153,7 +153,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleBoolean() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "boolean" to NonNullableField(Boolean::class.javaPrimitiveType!!) ))) @@ -166,7 +166,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleBooleanNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "boolean" to NullableField(Boolean::class.javaObjectType) ))) @@ -179,7 +179,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleBooleanNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "boolean" to NullableField(Boolean::class.javaObjectType) ))) @@ -192,7 +192,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleDouble() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "double" to NonNullableField(Double::class.javaPrimitiveType!!) ))) @@ -205,7 +205,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleDoubleNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "double" to NullableField(Double::class.javaObjectType) ))) @@ -218,7 +218,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleDoubleNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "double" to NullableField(Double::class.javaObjectType) ))) @@ -231,7 +231,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleShort() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "short" to NonNullableField(Short::class.javaPrimitiveType!!) ))) @@ -244,7 +244,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleShortNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "short" to NullableField(Short::class.javaObjectType) ))) @@ -257,7 +257,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleShortNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "short" to NullableField(Short::class.javaObjectType) ))) @@ -270,7 +270,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleFloat() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "float" to NonNullableField(Float::class.javaPrimitiveType!!) ))) @@ -283,7 +283,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleFloatNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "float" to NullableField(Float::class.javaObjectType) ))) @@ -296,7 +296,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleFloatNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "float" to NullableField(Float::class.javaObjectType) ))) @@ -309,7 +309,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleByte() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "byte" to NonNullableField(Byte::class.javaPrimitiveType!!) ))) @@ -324,7 +324,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleByteNullable() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "byte" to NullableField(Byte::class.javaObjectType) ))) @@ -339,7 +339,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun singleByteNullableNull() { - val clazz = ClassCarpenter().build(ClassSchema("single", mapOf( + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf( "byte" to NullableField(Byte::class.javaObjectType) ))) @@ -353,11 +353,10 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun simpleTypeKnownInterface() { val clazz = ClassCarpenter().build (ClassSchema( - "oneType", mapOf("name" to NonNullableField(String::class.java)), + testName(), mapOf("name" to NonNullableField(String::class.java)), interfaces = listOf (I::class.java))) val testVal = "Some Person" val classInstance = clazz.constructors[0].newInstance(testVal) - val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize(classInstance) val deserializedObj = DeserializationInput(sf2).deserialize(serialisedBytes) @@ -368,7 +367,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { @Test fun manyTypes() { - val manyClass = ClassCarpenter().build (ClassSchema("many", mapOf( + val manyClass = ClassCarpenter().build (ClassSchema(testName(), mapOf( "intA" to NonNullableField (Int::class.java), "intB" to NullableField (Integer::class.java), "intC" to NullableField (Integer::class.java), @@ -397,7 +396,7 @@ class DeserializeNeedingCarpentrySimpleTypesTest { "byteB" to NullableField (Byte::class.javaObjectType), "byteC" to NullableField (Byte::class.javaObjectType)))) - val serialisedBytes = TestSerializationOutput(VERBOSE, sf).serialize( + val serialisedBytes = TestSerializationOutput(VERBOSE, factory).serialize( manyClass.constructors.first().newInstance( 1, 2, null, "a", "b", null, diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt index d0cae9e3b6..3b8b2a5ec0 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeNeedingCarpentryTests.kt @@ -1,24 +1,25 @@ package net.corda.nodeapi.internal.serialization.amqp +import net.corda.core.serialization.CordaSerializable import org.junit.Test import kotlin.test.* import net.corda.nodeapi.internal.serialization.carpenter.* +import net.corda.nodeapi.internal.serialization.AllWhitelist +@CordaSerializable interface I { fun getName() : String } -/** - * These tests work by having the class carpenter build the classes we serialise and then deserialise them - * within the context of a second serialiser factory. The second factory is required as the first, having - * been used to serialise the class, will have cached a copy of the class and will thus bypass the need - * to pull it out of the class loader. - * - * However, those classes don't exist within the system's Class Loader and thus the deserialiser will be forced - * to carpent versions of them up using its own internal class carpenter (each carpenter houses it's own loader). This - * replicates the situation where a receiver doesn't have some or all elements of a schema present on it's classpath - */ -class DeserializeNeedingCarpentryTests { +// These tests work by having the class carpenter build the classes we serialise and then deserialise them +// within the context of a second serialiser factory. The second factory is required as the first, having +// been used to serialise the class, will have cached a copy of the class and will thus bypass the need +// to pull it out of the class loader. +// +// However, those classes don't exist within the system's Class Loader and thus the deserialiser will be forced +// to carpent versions of them up using its own internal class carpenter (each carpenter houses it's own loader). This +// replicates the situation where a receiver doesn't have some or all elements of a schema present on it's classpath +class DeserializeNeedingCarpentryTests : AmqpCarpenterBase() { companion object { /** * If you want to see the schema encoded into the envelope after serialisation change this to true @@ -26,13 +27,13 @@ class DeserializeNeedingCarpentryTests { private const val VERBOSE = false } - val sf1 = SerializerFactory() - val sf2 = SerializerFactory() + val sf1 = testDefaultFactory() + val sf2 = testDefaultFactoryWithWhitelist() // Deserialize with whitelisting on to check that `CordaSerializable` annotation present. @Test fun verySimpleType() { val testVal = 10 - val clazz = ClassCarpenter().build(ClassSchema("oneType", mapOf("a" to NonNullableField(Int::class.java)))) + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf("a" to NonNullableField(Int::class.java)))) val classInstance = clazz.constructors[0].newInstance(testVal) val serialisedBytes = TestSerializationOutput(VERBOSE, sf1).serialize(classInstance) @@ -66,16 +67,20 @@ class DeserializeNeedingCarpentryTests { val testValA = 10 val testValB = 20 val testValC = 20 - val clazz = ClassCarpenter().build(ClassSchema("oneType", mapOf("a" to NonNullableField(Int::class.java)))) + val clazz = ClassCarpenter().build(ClassSchema("${testName()}_clazz", + mapOf("a" to NonNullableField(Int::class.java)))) + val concreteA = clazz.constructors[0].newInstance(testValA) val concreteB = clazz.constructors[0].newInstance(testValB) val concreteC = clazz.constructors[0].newInstance(testValC) - val deserialisedA = DeserializationInput(sf2).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(concreteA)) + val deserialisedA = DeserializationInput(sf2).deserialize( + TestSerializationOutput(VERBOSE, sf1).serialize(concreteA)) assertEquals (testValA, deserialisedA::class.java.getMethod("getA").invoke(deserialisedA)) - val deserialisedB = DeserializationInput(sf2).deserialize(TestSerializationOutput(VERBOSE, sf1).serialize(concreteB)) + val deserialisedB = DeserializationInput(sf2).deserialize( + TestSerializationOutput(VERBOSE, sf1).serialize(concreteB)) assertEquals (testValB, deserialisedA::class.java.getMethod("getA").invoke(deserialisedB)) assertEquals (deserialisedA::class.java, deserialisedB::class.java) @@ -84,8 +89,10 @@ class DeserializeNeedingCarpentryTests { // won't already exist and it will be carpented a second time showing that when A and B are the // same underlying class that we didn't create a second instance of the class with the // second deserialisation - val lsf = SerializerFactory() - val deserialisedC = DeserializationInput(lsf).deserialize(TestSerializationOutput(VERBOSE, lsf).serialize(concreteC)) + val lfactory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + val deserialisedC = DeserializationInput(lfactory).deserialize( + TestSerializationOutput(VERBOSE, lfactory).serialize(concreteC)) + assertEquals (testValC, deserialisedC::class.java.getMethod("getA").invoke(deserialisedC)) assertNotEquals (deserialisedA::class.java, deserialisedC::class.java) assertNotEquals (deserialisedB::class.java, deserialisedC::class.java) @@ -94,7 +101,7 @@ class DeserializeNeedingCarpentryTests { @Test fun simpleTypeKnownInterface() { val clazz = ClassCarpenter().build (ClassSchema( - "oneType", mapOf("name" to NonNullableField(String::class.java)), + testName(), mapOf("name" to NonNullableField(String::class.java)), interfaces = listOf (I::class.java))) val testVal = "Some Person" val classInstance = clazz.constructors[0].newInstance(testVal) @@ -108,8 +115,9 @@ class DeserializeNeedingCarpentryTests { @Test fun arrayOfTypes() { - val clazz = ClassCarpenter().build(ClassSchema("oneType", mapOf("a" to NonNullableField(Int::class.java)))) + val clazz = ClassCarpenter().build(ClassSchema(testName(), mapOf("a" to NonNullableField(Int::class.java)))) + @CordaSerializable data class Outer (val a : Array) val outer = Outer (arrayOf ( @@ -142,8 +150,8 @@ class DeserializeNeedingCarpentryTests { fun reusedClasses() { val cc = ClassCarpenter() - val innerType = cc.build(ClassSchema("inner", mapOf("a" to NonNullableField(Int::class.java)))) - val outerType = cc.build(ClassSchema("outer", mapOf("a" to NonNullableField(innerType)))) + val innerType = cc.build(ClassSchema("${testName()}.inner", mapOf("a" to NonNullableField(Int::class.java)))) + val outerType = cc.build(ClassSchema("${testName()}.outer", mapOf("a" to NonNullableField(innerType)))) val inner = innerType.constructors[0].newInstance(1) val outer = outerType.constructors[0].newInstance(innerType.constructors[0].newInstance(2)) @@ -180,6 +188,7 @@ class DeserializeNeedingCarpentryTests { val nestedClass = cc.build (ClassSchema("nestedType", mapOf("name" to NonNullableField(String::class.java)))) + @CordaSerializable data class outer(val a: Any, val b: Any) val classInstance = outer ( @@ -195,10 +204,11 @@ class DeserializeNeedingCarpentryTests { @Test fun listOfType() { - val unknownClass = ClassCarpenter().build (ClassSchema("unknownClass", mapOf( + val unknownClass = ClassCarpenter().build (ClassSchema(testName(), mapOf( "v1" to NonNullableField(Int::class.java), "v2" to NonNullableField(Int::class.java)))) + @CordaSerializable data class outer (val l : List) val toSerialise = outer (listOf ( unknownClass.constructors.first().newInstance(1, 2), @@ -223,7 +233,7 @@ class DeserializeNeedingCarpentryTests { "gen.Interface", mapOf("age" to NonNullableField (Int::class.java)))) - val concreteClass = cc.build (ClassSchema ("gen.Class", mapOf( + val concreteClass = cc.build (ClassSchema (testName(), mapOf( "age" to NonNullableField (Int::class.java), "name" to NonNullableField(String::class.java)), interfaces = listOf (I::class.java, interfaceClass))) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt index 1e7171b31a..cc05b9b39f 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializeSimpleTypesTests.kt @@ -16,30 +16,30 @@ class DeserializeSimpleTypesTests { private const val VERBOSE = false } - val sf1 = SerializerFactory() - val sf2 = SerializerFactory() + val sf1 = testDefaultFactory() + val sf2 = testDefaultFactory() @Test fun testChar() { data class C(val c: Char) - var deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('c'))) + var deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('c'))) assertEquals('c', deserializedC.c) // CYRILLIC CAPITAL LETTER YU (U+042E) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('Ю'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('Ю'))) assertEquals('Ю', deserializedC.c) // ARABIC LETTER FEH WITH DOT BELOW (U+06A3) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('ڣ'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('ڣ'))) assertEquals('ڣ', deserializedC.c) // ARABIC LETTER DAD WITH DOT BELOW (U+06FB) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('ۻ'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('ۻ'))) assertEquals('ۻ', deserializedC.c) // BENGALI LETTER AA (U+0986) - deserializedC = DeserializationInput().deserialize(SerializationOutput().serialize(C('আ'))) + deserializedC = DeserializationInput(sf1).deserialize(SerializationOutput(sf1).serialize(C('আ'))) assertEquals('আ', deserializedC.c) } @@ -49,8 +49,8 @@ class DeserializeSimpleTypesTests { data class C(val c: Character) val c = C(Character('c')) - val serialisedC = SerializationOutput().serialize(c) - val deserializedC = DeserializationInput().deserialize(serialisedC) + val serialisedC = SerializationOutput(sf1).serialize(c) + val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) assertEquals(c.c, deserializedC.c) } @@ -60,8 +60,8 @@ class DeserializeSimpleTypesTests { data class C(val c: Char?) val c = C(null) - val serialisedC = SerializationOutput().serialize(c) - val deserializedC = DeserializationInput().deserialize(serialisedC) + val serialisedC = SerializationOutput(sf1).serialize(c) + val deserializedC = DeserializationInput(sf1).deserialize(serialisedC) assertEquals(c.c, deserializedC.c) } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedTypeTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedTypeTests.kt index 469127061d..5be8f67d57 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedTypeTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/DeserializedParameterizedTypeTests.kt @@ -34,6 +34,11 @@ class DeserializedParameterizedTypeTests { verify("java.util.Map ") } + @Test + fun `test list of commands`() { + verify("java.util.List>") + } + @Test(expected = NotSerializableException::class) fun `test trailing text`() { verify("java.util.Mapfoo") diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt new file mode 100644 index 0000000000..fd8e6b3058 --- /dev/null +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.kt @@ -0,0 +1,461 @@ +package net.corda.nodeapi.internal.serialization.amqp + +import net.corda.core.serialization.SerializedBytes +import net.corda.core.serialization.DeprecatedConstructorForDeserialization + +import org.junit.Test +import java.io.File +import java.io.NotSerializableException +import kotlin.test.assertEquals + +// To regenerate any of the binary test files do the following +// +// 1. Uncomment the code where the original form of the class is defined in the test +// 2. Comment out the rest of the test +// 3. Run the test +// 4. Using the printed path copy that file to the resources directory +// 5. Comment back out the generation code and uncomment the actual test +class EvolvabilityTests { + + @Test + fun simpleOrderSwapSameType() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapSameType") + val f = File(path.toURI()) + + val A = 1 + val B = 2 + + // Original version of the class for the serialised version of this class + // + // data class C (val a: Int, val b: Int) + // val sc = SerializationOutput(sf).serialize(C(A, B)) + // f.writeBytes(sc.bytes) + + // new version of the class, in this case the order of the parameters has been swapped + data class C (val b: Int, val a: Int) + + val sc2 = f.readBytes() + val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals(A, deserializedC.a) + assertEquals(B, deserializedC.b) + } + + @Test + fun simpleOrderSwapDifferentType() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.simpleOrderSwapDifferentType") + val f = File(path.toURI()) + val A = 1 + val B = "two" + + // Original version of the class as it was serialised + // + // data class C (val a: Int, val b: String) + // val sc = SerializationOutput(sf).serialize(C(A, B)) + // f.writeBytes(sc.bytes) + + // new version of the class, in this case the order of the parameters has been swapped + data class C (val b: String, val a: Int) + + val sc2 = f.readBytes() + val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals(A, deserializedC.a) + assertEquals(B, deserializedC.b) + } + + @Test + fun addAdditionalParamNotMandatory() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParamNotMandatory") + val f = File(path.toURI()) + val A = 1 + + // Original version of the class as it was serialised + // + // data class C(val a: Int) + // val sc = SerializationOutput(sf).serialize(C(A)) + // f.writeBytes(sc.bytes) + // println ("Path = $path") + + data class C (val a: Int, val b: Int?) + + val sc2 = f.readBytes() + val deserializedC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals (A, deserializedC.a) + assertEquals (null, deserializedC.b) + } + + @Test(expected = NotSerializableException::class) + fun addAdditionalParam() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAdditionalParam") + val f = File(path.toURI()) + @Suppress("UNUSED_VARIABLE") + val A = 1 + + // Original version of the class as it was serialised + // + // data class C(val a: Int) + // val sc = SerializationOutput(sf).serialize(C(A)) + // f.writeBytes(sc.bytes) + // println ("Path = $path") + + // new version of the class, in this case a new parameter has been added (b) + data class C (val a: Int, val b: Int) + + val sc2 = f.readBytes() + + // Expected to throw as we can't construct the new type as it contains a newly + // added parameter that isn't optional, i.e. not nullable and there isn't + // a constructor that takes the old parameters + DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + } + + @Suppress("UNUSED_VARIABLE") + @Test + fun removeParameters() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.removeParameters") + val f = File(path.toURI()) + val A = 1 + val B = "two" + val C = "three" + val D = 4 + + // Original version of the class as it was serialised + // + // data class CC(val a: Int, val b: String, val c: String, val d: Int) + // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) + // f.writeBytes(scc.bytes) + // println ("Path = $path") + + data class CC (val b: String, val d: Int) + + val sc2 = f.readBytes() + val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals (B, deserializedCC.b) + assertEquals (D, deserializedCC.d) + } + + @Suppress("UNUSED_VARIABLE") + @Test + fun addAndRemoveParameters() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addAndRemoveParameters") + val f = File(path.toURI()) + val A = 1 + val B = "two" + val C = "three" + val D = 4 + val E = null + + // Original version of the class as it was serialised + // + // data class CC(val a: Int, val b: String, val c: String, val d: Int) + // val scc = SerializationOutput(sf).serialize(CC(A, B, C, D)) + // f.writeBytes(scc.bytes) + // println ("Path = $path") + + data class CC(val a: Int, val e: Boolean?, val d: Int) + + val sc2 = f.readBytes() + val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals(A, deserializedCC.a) + assertEquals(E, deserializedCC.e) + assertEquals(D, deserializedCC.d) + } + + @Test + fun addMandatoryFieldWithAltConstructor() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.addMandatoryFieldWithAltConstructor") + val f = File(path.toURI()) + val A = 1 + + // Original version of the class as it was serialised + // + // data class CC(val a: Int) + // val scc = SerializationOutput(sf).serialize(CC(A)) + // f.writeBytes(scc.bytes) + // println ("Path = $path") + + @Suppress("UNUSED") + data class CC (val a: Int, val b: String) { + @DeprecatedConstructorForDeserialization(1) + constructor (a: Int) : this (a, "hello") + } + + val sc2 = f.readBytes() + val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals (A, deserializedCC.a) + assertEquals ("hello", deserializedCC.b) + } + + @Test(expected = NotSerializableException::class) + @Suppress("UNUSED") + fun addMandatoryFieldWithAltConstructorUnAnnotated() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource( + "EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated") + val f = File(path.toURI()) + @Suppress("UNUSED_VARIABLE") + val A = 1 + + // Original version of the class as it was serialised + // + // data class CC(val a: Int) + // val scc = SerializationOutput(sf).serialize(CC(A)) + // f.writeBytes(scc.bytes) + // println ("Path = $path") + + data class CC (val a: Int, val b: String) { + // constructor annotation purposefully omitted + constructor (a: Int) : this (a, "hello") + } + + // we expect this to throw as we should not find any constructors + // capable of dealing with this + DeserializationInput(sf).deserialize(SerializedBytes(f.readBytes())) + } + + @Test + fun addMandatoryFieldWithAltReorderedConstructor() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource( + "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor") + val f = File(path.toURI()) + val A = 1 + val B = 100 + val C = "This is not a banana" + + // Original version of the class as it was serialised + // + // data class CC(val a: Int, val b: Int, val c: String) + // val scc = SerializationOutput(sf).serialize(CC(A, B, C)) + // f.writeBytes(scc.bytes) + // println ("Path = $path") + + @Suppress("UNUSED") + data class CC (val a: Int, val b: Int, val c: String, val d: String) { + // ensure none of the original parameters align with the initial + // construction order + @DeprecatedConstructorForDeserialization(1) + constructor (c: String, a: Int, b: Int) : this (a, b, c, "wibble") + } + + val sc2 = f.readBytes() + val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals (A, deserializedCC.a) + assertEquals (B, deserializedCC.b) + assertEquals (C, deserializedCC.c) + assertEquals ("wibble", deserializedCC.d) + } + + @Test + fun addMandatoryFieldWithAltReorderedConstructorAndRemoval() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource( + "EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval") + val f = File(path.toURI()) + val A = 1 + @Suppress("UNUSED_VARIABLE") + val B = 100 + val C = "This is not a banana" + + // Original version of the class as it was serialised + // + // data class CC(val a: Int, val b: Int, val c: String) + // val scc = SerializationOutput(sf).serialize(CC(A, B, C)) + // f.writeBytes(scc.bytes) + // println ("Path = $path") + + // b is removed, d is added + data class CC (val a: Int, val c: String, val d: String) { + // ensure none of the original parameters align with the initial + // construction order + @Suppress("UNUSED") + @DeprecatedConstructorForDeserialization(1) + constructor (c: String, a: Int) : this (a, c, "wibble") + } + + val sc2 = f.readBytes() + val deserializedCC = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals (A, deserializedCC.a) + assertEquals (C, deserializedCC.c) + assertEquals ("wibble", deserializedCC.d) + } + + @Test + fun multiVersion() { + val sf = testDefaultFactory() + val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.1") + val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.2") + val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersion.3") + + @Suppress("UNUSED_VARIABLE") + val f = File(path1.toURI()) + + val a = 100 + val b = 200 + val c = 300 + val d = 400 + + // Original version of the class as it was serialised + // + // Version 1: + // data class C (val a: Int, val b: Int) + // Version 2 - add param c + // data class C (val c: Int, val b: Int, val a: Int) + // Version 3 - add param d + // data class C (val b: Int, val c: Int, val d: Int, val a: Int) + // + // val scc = SerializationOutput(sf).serialize(C(b, c, d, a)) + // f.writeBytes(scc.bytes) + // println ("Path = $path1") + + @Suppress("UNUSED") + data class C (val e: Int, val c: Int, val b: Int, val a: Int, val d: Int) { + @DeprecatedConstructorForDeserialization(1) + constructor (b: Int, a: Int) : this (-1, -1, b, a, -1) + @DeprecatedConstructorForDeserialization(2) + constructor (a: Int, c: Int, b: Int) : this (-1, c, b, a, -1) + @DeprecatedConstructorForDeserialization(3) + constructor (a: Int, b: Int, c: Int, d: Int) : this (-1, c, b, a, d) + } + + val sb1 = File(path1.toURI()).readBytes() + val db1 = DeserializationInput(sf).deserialize(SerializedBytes(sb1)) + + assertEquals(a, db1.a) + assertEquals(b, db1.b) + assertEquals(-1, db1.c) + assertEquals(-1, db1.d) + assertEquals(-1, db1.e) + + val sb2 = File(path2.toURI()).readBytes() + val db2 = DeserializationInput(sf).deserialize(SerializedBytes(sb2)) + + assertEquals(a, db2.a) + assertEquals(b, db2.b) + assertEquals(c, db2.c) + assertEquals(-1, db2.d) + assertEquals(-1, db2.e) + + val sb3 = File(path3.toURI()).readBytes() + val db3 = DeserializationInput(sf).deserialize(SerializedBytes(sb3)) + + assertEquals(a, db3.a) + assertEquals(b, db3.b) + assertEquals(c, db3.c) + assertEquals(d, db3.d) + assertEquals(-1, db3.e) + } + + @Test + fun changeSubType() { + val sf = testDefaultFactory() + val path = EvolvabilityTests::class.java.getResource("EvolvabilityTests.changeSubType") + val f = File(path.toURI()) + val oa = 100 + val ia = 200 + + // Original version of the class as it was serialised + // + // data class Inner (val a: Int) + // data class Outer (val a: Int, val b: Inner) + // val scc = SerializationOutput(sf).serialize(Outer(oa, Inner (ia))) + // f.writeBytes(scc.bytes) + // println ("Path = $path") + + // Add a parameter to inner but keep outer unchanged + data class Inner (val a: Int, val b: String?) + data class Outer (val a: Int, val b: Inner) + + val sc2 = f.readBytes() + val outer = DeserializationInput(sf).deserialize(SerializedBytes(sc2)) + + assertEquals (oa, outer.a) + assertEquals (ia, outer.b.a) + assertEquals (null, outer.b.b) + } + + @Test + fun multiVersionWithRemoval() { + val sf = testDefaultFactory() + val path1 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.1") + val path2 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.2") + val path3 = EvolvabilityTests::class.java.getResource("EvolvabilityTests.multiVersionWithRemoval.3") + + @Suppress("UNUSED_VARIABLE") + val a = 100 + val b = 200 + val c = 300 + val d = 400 + val e = 500 + val f = 600 + + // Original version of the class as it was serialised + // + // Version 1: + // data class C (val a: Int, val b: Int, val c: Int) + // Version 2 - add param c + // data class C (val b: Int, val c: Int, val d: Int, val e: Int) + // Version 3 - add param d + // data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int) + // + // val scc = SerializationOutput(sf).serialize(C(b, c, d, e, f)) + // File(path1.toURI()).writeBytes(scc.bytes) + // println ("Path = $path1") + + @Suppress("UNUSED") + data class C (val b: Int, val c: Int, val d: Int, val e: Int, val f: Int, val g: Int) { + @DeprecatedConstructorForDeserialization(1) + constructor (b: Int, c: Int) : this (b, c, -1, -1, -1, -1) + @DeprecatedConstructorForDeserialization(2) + constructor (b: Int, c: Int, d: Int) : this (b, c, d, -1, -1, -1) + @DeprecatedConstructorForDeserialization(3) + constructor (b: Int, c: Int, d: Int, e: Int) : this (b, c, d, e, -1, -1) + @DeprecatedConstructorForDeserialization(4) + constructor (b: Int, c: Int, d: Int, e: Int, f: Int) : this (b, c, d, e, f, -1) + } + + val sb1 = File(path1.toURI()).readBytes() + val db1 = DeserializationInput(sf).deserialize(SerializedBytes(sb1)) + + assertEquals(b, db1.b) + assertEquals(c, db1.c) + assertEquals(-1, db1.d) // must not be set by calling constructor 2 by mistake + assertEquals(-1, db1.e) + assertEquals(-1, db1.f) + assertEquals(-1, db1.g) + + val sb2 = File(path2.toURI()).readBytes() + val db2 = DeserializationInput(sf).deserialize(SerializedBytes(sb2)) + + assertEquals(b, db2.b) + assertEquals(c, db2.c) + assertEquals(d, db2.d) + assertEquals(e, db2.e) + assertEquals(-1, db2.f) + assertEquals(-1, db1.g) + + val sb3 = File(path3.toURI()).readBytes() + val db3 = DeserializationInput(sf).deserialize(SerializedBytes(sb3)) + + assertEquals(b, db3.b) + assertEquals(c, db3.c) + assertEquals(d, db3.d) + assertEquals(e, db3.e) + assertEquals(f, db3.f) + assertEquals(-1, db3.g) + } +} \ No newline at end of file diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt index 53bdaaafe1..3e83fe6f62 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializationOutputTests.kt @@ -14,19 +14,24 @@ import net.corda.nodeapi.RPCException import net.corda.nodeapi.internal.serialization.AbstractAMQPSerializationScheme import net.corda.nodeapi.internal.serialization.EmptyWhitelist import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory.Companion.isPrimitive -import net.corda.nodeapi.internal.serialization.amqp.custom.* +import net.corda.nodeapi.internal.serialization.AllWhitelist +import net.corda.testing.BOB_IDENTITY import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_PUBKEY import org.apache.qpid.proton.amqp.* import org.apache.qpid.proton.codec.DecoderImpl import org.apache.qpid.proton.codec.EncoderImpl +import org.junit.Ignore import org.junit.Test import java.io.IOException import java.io.NotSerializableException import java.math.BigDecimal import java.nio.ByteBuffer -import java.time.Instant +import java.time.* +import java.time.temporal.ChronoUnit +import java.time.temporal.TemporalUnit import java.util.* +import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -94,35 +99,35 @@ class SerializationOutputTests { data class SortedSetWrapper(val set: SortedSet) - open class InheritedGeneric(val foo: X) + open class InheritedGeneric(val foo: X) data class ExtendsGeneric(val bar: Int, val pub: String) : InheritedGeneric(pub) - interface GenericInterface { + interface GenericInterface { val pub: X } data class ImplementsGenericString(val bar: Int, override val pub: String) : GenericInterface - data class ImplementsGenericX(val bar: Int, override val pub: Y) : GenericInterface + data class ImplementsGenericX(val bar: Int, override val pub: Y) : GenericInterface - abstract class AbstractGenericX : GenericInterface + abstract class AbstractGenericX : GenericInterface - data class InheritGenericX(val duke: Double, override val pub: A) : AbstractGenericX() + data class InheritGenericX(val duke: Double, override val pub: A) : AbstractGenericX() data class CapturesGenericX(val foo: GenericInterface) object KotlinObject class Mismatch(fred: Int) { - val ginger: Int = fred + private val ginger: Int = fred override fun equals(other: Any?): Boolean = (other as? Mismatch)?.ginger == ginger override fun hashCode(): Int = ginger } class MismatchType(fred: Long) { - val ginger: Int = fred.toInt() + private val ginger: Int = fred.toInt() override fun equals(other: Any?): Boolean = (other as? MismatchType)?.ginger == ginger override fun hashCode(): Int = ginger @@ -136,8 +141,10 @@ class SerializationOutputTests { data class PolymorphicProperty(val foo: FooInterface?) private fun serdes(obj: Any, - factory: SerializerFactory = SerializerFactory(), - freshDeserializationFactory: SerializerFactory = SerializerFactory(), + factory: SerializerFactory = SerializerFactory ( + AllWhitelist, ClassLoader.getSystemClassLoader()), + freshDeserializationFactory: SerializerFactory = SerializerFactory( + AllWhitelist, ClassLoader.getSystemClassLoader()), expectedEqual: Boolean = true, expectDeserializedEqual: Boolean = true): Any { val ser = SerializationOutput(factory) @@ -242,7 +249,7 @@ class SerializationOutputTests { @Test(expected = IllegalArgumentException::class) fun `test dislike of HashMap`() { - val obj = WrapHashMap(HashMap()) + val obj = WrapHashMap(HashMap()) serdes(obj) } @@ -285,13 +292,13 @@ class SerializationOutputTests { @Test(expected = NotSerializableException::class) fun `test whitelist`() { val obj = Woo2(4) - serdes(obj, SerializerFactory(EmptyWhitelist)) + serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) } @Test fun `test annotation whitelisting`() { val obj = AnnotatedWoo(5) - serdes(obj, SerializerFactory(EmptyWhitelist)) + serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) } @Test(expected = NotSerializableException::class) @@ -352,9 +359,9 @@ class SerializationOutputTests { serdes(obj) } - @Test(expected = NotSerializableException::class) + @Test fun `test TreeMap property`() { - val obj = TreeMapWrapper(TreeMap()) + val obj = TreeMapWrapper(TreeMap()) obj.tree[456] = Foo("Fred", 123) serdes(obj) } @@ -387,10 +394,10 @@ class SerializationOutputTests { @Test fun `test custom serializers on public key`() { - val factory = SerializerFactory() - factory.register(PublicKeySerializer) - val factory2 = SerializerFactory() - factory2.register(PublicKeySerializer) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.PublicKeySerializer) val obj = MEGA_CORP_PUBKEY serdes(obj, factory, factory2) } @@ -398,16 +405,16 @@ class SerializationOutputTests { @Test fun `test annotation is inherited`() { val obj = InheritAnnotation("blah") - serdes(obj, SerializerFactory(EmptyWhitelist)) + serdes(obj, SerializerFactory(EmptyWhitelist, ClassLoader.getSystemClassLoader())) } @Test fun `test throwables serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) val t = IllegalAccessException("message").fillInStackTrace() val desThrowable = serdes(t, factory, factory2, false) as Throwable @@ -416,11 +423,11 @@ class SerializationOutputTests { @Test fun `test complex throwables serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) try { try { @@ -434,11 +441,10 @@ class SerializationOutputTests { } } - fun assertSerializedThrowableEquivalent(t: Throwable, desThrowable: Throwable) { + private fun assertSerializedThrowableEquivalent(t: Throwable, desThrowable: Throwable) { assertTrue(desThrowable is CordaRuntimeException) // Since we don't handle the other case(s) yet if (desThrowable is CordaRuntimeException) { assertEquals("${t.javaClass.name}: ${t.message}", desThrowable.message) - assertTrue(desThrowable is CordaRuntimeException) assertTrue(Objects.deepEquals(t.stackTrace, desThrowable.stackTrace)) assertEquals(t.suppressed.size, desThrowable.suppressed.size) t.suppressed.zip(desThrowable.suppressed).forEach { (before, after) -> assertSerializedThrowableEquivalent(before, after) } @@ -447,11 +453,11 @@ class SerializationOutputTests { @Test fun `test suppressed throwables serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) try { try { @@ -469,11 +475,11 @@ class SerializationOutputTests { @Test fun `test flow corda exception subclasses serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) val obj = FlowException("message").fillInStackTrace() serdes(obj, factory, factory2) @@ -481,11 +487,11 @@ class SerializationOutputTests { @Test fun `test RPC corda exception subclasses serialize`() { - val factory = SerializerFactory() - factory.register(ThrowableSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(ThrowableSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ThrowableSerializer(factory2)) val obj = RPCException("message").fillInStackTrace() serdes(obj, factory, factory2) @@ -512,8 +518,6 @@ class SerializationOutputTests { override fun verify(tx: LedgerTransaction) { } - - override val legalContractReference: SecureHash = SecureHash.Companion.sha256("FooContractLegal") } class FooState : ContractState { @@ -525,12 +529,12 @@ class SerializationOutputTests { @Test fun `test transaction state`() { - val state = TransactionState(FooState(), MEGA_CORP) + val state = TransactionState(FooState(), MEGA_CORP) - val factory = SerializerFactory() + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) AbstractAMQPSerializationScheme.registerCustomSerializers(factory) - val factory2 = SerializerFactory() + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) AbstractAMQPSerializationScheme.registerCustomSerializers(factory2) val desState = serdes(state, factory, factory2, expectedEqual = false, expectDeserializedEqual = false) @@ -542,11 +546,11 @@ class SerializationOutputTests { @Test fun `test currencies serialize`() { - val factory = SerializerFactory() - factory.register(CurrencySerializer) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer) - val factory2 = SerializerFactory() - factory2.register(CurrencySerializer) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.CurrencySerializer) val obj = Currency.getInstance("USD") serdes(obj, factory, factory2) @@ -554,11 +558,11 @@ class SerializationOutputTests { @Test fun `test big decimals serialize`() { - val factory = SerializerFactory() - factory.register(BigDecimalSerializer) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) - val factory2 = SerializerFactory() - factory2.register(BigDecimalSerializer) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.BigDecimalSerializer) val obj = BigDecimal("100000000000000000000000000000.00") serdes(obj, factory, factory2) @@ -566,23 +570,195 @@ class SerializationOutputTests { @Test fun `test instants serialize`() { - val factory = SerializerFactory() - factory.register(InstantSerializer(factory)) + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(InstantSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.InstantSerializer(factory2)) val obj = Instant.now() serdes(obj, factory, factory2) } @Test - fun `test StateRef serialize`() { - val factory = SerializerFactory() - factory.register(InstantSerializer(factory)) + fun `test durations serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.DurationSerializer(factory)) - val factory2 = SerializerFactory() - factory2.register(InstantSerializer(factory2)) + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.DurationSerializer(factory2)) + + val obj = Duration.of(1000000L, ChronoUnit.MILLIS) + serdes(obj, factory, factory2) + } + + @Test + fun `test local date serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateSerializer(factory2)) + + val obj = LocalDate.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test local time serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalTimeSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalTimeSerializer(factory2)) + + val obj = LocalTime.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test local date time serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateTimeSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.LocalDateTimeSerializer(factory2)) + + val obj = LocalDateTime.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test zoned date time serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.ZonedDateTimeSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.ZonedDateTimeSerializer(factory2)) + + val obj = ZonedDateTime.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test offset time serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetTimeSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetTimeSerializer(factory2)) + + val obj = OffsetTime.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test offset date time serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetDateTimeSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.OffsetDateTimeSerializer(factory2)) + + val obj = OffsetDateTime.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test year serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.YearSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.YearSerializer(factory2)) + + val obj = Year.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test year month serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.YearMonthSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.YearMonthSerializer(factory2)) + + val obj = YearMonth.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test month day serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.MonthDaySerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.MonthDaySerializer(factory2)) + + val obj = MonthDay.now() + serdes(obj, factory, factory2) + } + + @Test + fun `test period serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.PeriodSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.PeriodSerializer(factory2)) + + val obj = Period.of(99, 98, 97) + serdes(obj, factory, factory2) + } + + // TODO: ignored due to Proton-J bug https://issues.apache.org/jira/browse/PROTON-1551 + @Ignore + @Test + fun `test certificate holder serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CertificateHolderSerializer) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.X509CertificateHolderSerializer) + + val obj = BOB_IDENTITY.certificate + serdes(obj, factory, factory2) + } + + // TODO: ignored due to Proton-J bug https://issues.apache.org/jira/browse/PROTON-1551 + @Ignore + @Test + fun `test party and certificate serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory.register(net.corda.nodeapi.internal.serialization.amqp.custom.PartyAndCertificateSerializer(factory)) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + factory2.register(net.corda.nodeapi.internal.serialization.amqp.custom.PartyAndCertificateSerializer(factory2)) + + val obj = BOB_IDENTITY + serdes(obj, factory, factory2) + } + + class OtherGeneric + + open class GenericSuperclass(val param: OtherGeneric) + + class GenericSubclass(param: OtherGeneric) : GenericSuperclass(param) { + override fun equals(other: Any?): Boolean = other is GenericSubclass // This is a bit lame but we just want to check it doesn't throw exceptions + } + + @Test + fun `test generic in constructor serialize`() { + val obj = GenericSubclass(OtherGeneric()) + serdes(obj) + } + + @Test + fun `test StateRef serialize`() { + val factory = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) + + val factory2 = SerializerFactory(AllWhitelist, ClassLoader.getSystemClassLoader()) val obj = StateRef(SecureHash.randomSHA256(), 0) serdes(obj, factory, factory2) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt index 3aae840918..b7f1c2d348 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/ClassCarpenterTestUtils.kt @@ -1,11 +1,8 @@ package net.corda.nodeapi.internal.serialization.carpenter +import net.corda.nodeapi.internal.serialization.amqp.* import net.corda.nodeapi.internal.serialization.amqp.Field import net.corda.nodeapi.internal.serialization.amqp.Schema -import net.corda.nodeapi.internal.serialization.amqp.TypeNotation -import net.corda.nodeapi.internal.serialization.amqp.CompositeType -import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory -import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput fun mangleName(name: String) = "${name}__carpenter" @@ -37,7 +34,7 @@ fun Schema.mangleNames(names: List): Schema { } open class AmqpCarpenterBase { - var factory = SerializerFactory() + var factory = testDefaultFactory() fun serialise(clazz: Any) = SerializationOutput(factory).serialize(clazz) fun testName(): String = Thread.currentThread().stackTrace[2].methodName diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt index 5ec40e0f4e..a61bee62f7 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/CompositeMemberCompositeSchemaToClassCarpenterTests.kt @@ -63,7 +63,7 @@ class CompositeMembers : AmqpCarpenterBase() { assertEquals("b", amqpSchemaB.fields[1].name) assertEquals("int", amqpSchemaB.fields[1].type) - val metaSchema = obj.envelope.schema.carpenterSchema() + val metaSchema = obj.envelope.schema.carpenterSchema(ClassLoader.getSystemClassLoader()) // if we know all the classes there is nothing to really achieve here assert(metaSchema.carpenterSchemas.isEmpty()) @@ -92,7 +92,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is B) - amqpSchema.carpenterSchema() + amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) } @Test @@ -112,7 +112,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is B) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("B"))) - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(1, carpenterSchema.size) @@ -140,7 +140,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is B) val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // just verify we're in the expected initial state, A is carpentable, B is not because // it depends on A and the dependency chains are in place @@ -200,7 +200,7 @@ class CompositeMembers : AmqpCarpenterBase() { val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - amqpSchema.carpenterSchema() + amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) } @Test(expected = UncarpentableException::class) @@ -226,7 +226,7 @@ class CompositeMembers : AmqpCarpenterBase() { val amqpSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - amqpSchema.carpenterSchema() + amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) } @Suppress("UNUSED") @@ -251,7 +251,7 @@ class CompositeMembers : AmqpCarpenterBase() { assert(obj.obj is C) val carpenterSchema = obj.envelope.schema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - TestMetaCarpenter(carpenterSchema.carpenterSchema()) + TestMetaCarpenter(carpenterSchema.carpenterSchema(ClassLoader.getSystemClassLoader())) } /* diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt index 8176823aa3..81cf727c58 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/InheritanceSchemaToClassCarpenterTests.kt @@ -45,14 +45,14 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertTrue(obj.obj is A) val serSchema = obj.envelope.schema assertEquals(2, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // since we're using an envelope generated by seilaising classes defined locally // it's extremely unlikely we'd need to carpent any classes assertEquals(0, l1.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(1, l2.size) val aSchema = l2.carpenterSchemas.find { it.name == mangleName(classTestName("A")) } @@ -88,13 +88,13 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(2, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(0, l1.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) val aName = mangleName(classTestName("A")) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) assertEquals(1, l2.size) @@ -132,7 +132,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(3, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // since we're using an envelope generated by serialising classes defined locally // it's extremely unlikely we'd need to carpent any classes @@ -141,7 +141,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { // pretend we don't know the class we've been sent, i.e. it's unknown to the class loader, and thus // needs some carpentry val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val aName = mangleName(classTestName("A")) assertEquals(1, l2.size) @@ -180,14 +180,14 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(3, serSchema.types.size) - val l1 = serSchema.carpenterSchema() + val l1 = serSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // since we're using an envelope generated by serialising classes defined locally // it's extremely unlikely we'd need to carpent any classes assertEquals(0, l1.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"))) - val l2 = mangleSchema.carpenterSchema() + val l2 = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val aName = mangleName(classTestName("A")) assertEquals(1, l2.size) @@ -235,7 +235,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals(4, serSchema.types.size) val mangleSchema = serSchema.mangleNames(listOf(classTestName("A"), classTestName("B"))) - val cSchema = mangleSchema.carpenterSchema() + val cSchema = mangleSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) val aName = mangleName(classTestName("A")) val bName = mangleName(classTestName("B")) @@ -292,7 +292,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { // ignore the return as we expect this to throw serSchema.mangleNames(listOf( - classTestName("A"), "${this.javaClass.`package`.name}.I")).carpenterSchema() + classTestName("A"), "${this.javaClass.`package`.name}.I")).carpenterSchema(ClassLoader.getSystemClassLoader()) } @Test @@ -315,7 +315,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { val amqpSchema = serSchema.mangleNames(listOf(classTestName("A"), "${this.javaClass.`package`.name}.I")) val aName = mangleName(classTestName("A")) val iName = mangleName("${this.javaClass.`package`.name}.I") - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // whilst there are two unknown classes within the envelope A depends on I so we can't construct a // schema for A until we have for I @@ -362,7 +362,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { val aName = mangleName(classTestName("A")) val iName = mangleName("${this.javaClass.`package`.name}.I") val iiName = mangleName("${this.javaClass.`package`.name}.II") - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // there is nothing preventing us from carpenting up the two interfaces so // our initial list should contain both interface with A being dependent on both @@ -414,7 +414,7 @@ class InheritanceSchemaToClassCarpenterTests : AmqpCarpenterBase() { val aName = mangleName(classTestName("A")) val iName = mangleName("${this.javaClass.`package`.name}.I") val iiiName = mangleName("${this.javaClass.`package`.name}.III") - val carpenterSchema = amqpSchema.carpenterSchema() + val carpenterSchema = amqpSchema.carpenterSchema(ClassLoader.getSystemClassLoader()) // Since A depends on III and III extends I we will have to construct them // in that reverse order (I -> III -> A) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt index d823e5a8e7..f9ea362882 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/MultiMemberCompositeSchemaToClassCarpenterTests.kt @@ -36,7 +36,10 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("int", amqpSchema.fields[1].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) assertEquals(1, carpenterSchema.size) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") } @@ -77,7 +80,10 @@ class MultiMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("string", amqpSchema.fields[1].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) assertEquals(1, carpenterSchema.size) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") } diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt index c68e222568..532e174903 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/carpenter/SingleMemberCompositeSchemaToClassCarpenterTests.kt @@ -30,7 +30,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("int", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -58,7 +61,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { val amqpSchema = obj.envelope.schema.types[0] as CompositeType val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -90,7 +96,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("long", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -122,7 +131,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("short", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -154,7 +166,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("double", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) @@ -186,7 +201,10 @@ class SingleMemberCompositeSchemaToClassCarpenterTests : AmqpCarpenterBase() { assertEquals("float", amqpSchema.fields[0].type) val carpenterSchema = CarpenterSchemas.newInstance() - amqpSchema.carpenterSchema(carpenterSchemas = carpenterSchema, force = true) + amqpSchema.carpenterSchema( + classloader = ClassLoader.getSystemClassLoader(), + carpenterSchemas = carpenterSchema, + force = true) val aSchema = carpenterSchema.carpenterSchemas.find { it.name == classTestName("A") }!! val aBuilder = ClassCarpenter().build(aSchema) diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParam b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParam new file mode 100644 index 0000000000..375f8642c2 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParam differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory new file mode 100644 index 0000000000..eb7fdfc160 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAdditionalParamNotMandatory differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters new file mode 100644 index 0000000000..5a45bb3e39 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addAndRemoveParameters differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor new file mode 100644 index 0000000000..a06dc1fbfc Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructor differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated new file mode 100644 index 0000000000..f77b8e5c4d Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltConstructorUnAnnotated differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor new file mode 100644 index 0000000000..7500dbd59b Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructor differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval new file mode 100644 index 0000000000..32d79fc258 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.addMandatoryFieldWithAltReorderedConstructorAndRemoval differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType new file mode 100644 index 0000000000..bcc0475b4a Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.changeSubType differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 new file mode 100644 index 0000000000..263bd776f7 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.1 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 new file mode 100644 index 0000000000..e11895f9fc Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.2 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 new file mode 100644 index 0000000000..1117d04f96 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersion.3 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 new file mode 100644 index 0000000000..a411a89c16 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.1 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 new file mode 100644 index 0000000000..f0eac50a93 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.2 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 new file mode 100644 index 0000000000..fdb3336d1c Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.multiVersionWithRemoval.3 differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters new file mode 100644 index 0000000000..375f8642c2 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.removeParameters differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType new file mode 100644 index 0000000000..4b6bf6a7b8 Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapDifferentType differ diff --git a/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType new file mode 100644 index 0000000000..b3852fe5ff Binary files /dev/null and b/node-api/src/test/resources/net/corda/nodeapi/internal/serialization/amqp/EvolvabilityTests.simpleOrderSwapSameType differ diff --git a/node-schemas/build.gradle b/node-schemas/build.gradle deleted file mode 100644 index 7f015714a1..0000000000 --- a/node-schemas/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'kotlin' -apply plugin: 'kotlin-kapt' -apply plugin: 'idea' -apply plugin: 'com.jfrog.artifactory' - -description 'Corda node database schemas' - -dependencies { - compile project(':core') - - // Requery: SQL based query & persistence for Kotlin - kapt "io.requery:requery-processor:$requery_version" - - testCompile project(':test-utils') - testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testCompile "junit:junit:$junit_version" - - // For H2 database support in persistence - testCompile "com.h2database:h2:$h2_version" -} - -sourceSets { - main { - kotlin { - srcDir "$buildDir/generated/source/kapt/main" - } - } -} - -jar { - baseName 'corda-node-schemas' -} - -publish { - name jar.baseName -} \ No newline at end of file diff --git a/node-schemas/generated/source/kapt/main/net/corda/node/services/vault/schemas/Models.java b/node-schemas/generated/source/kapt/main/net/corda/node/services/vault/schemas/Models.java deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/requery/AttachmentsSchema.kt b/node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/requery/AttachmentsSchema.kt deleted file mode 100644 index 1f756febfc..0000000000 --- a/node-schemas/src/main/kotlin/net/corda/node/services/persistence/schemas/requery/AttachmentsSchema.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.corda.node.services.persistence.schemas.requery - -import io.requery.* -import net.corda.core.crypto.SecureHash -import net.corda.core.schemas.requery.converters.BlobConverter - -@Table(name = "attachments") -@Entity(model = "persistence") -interface Attachment : Persistable { - - @get:Key - @get:Column(name = "att_id", index = true) - var attId: SecureHash - - @get:Column(name = "content") - @get:Convert(BlobConverter::class) - var content: ByteArray -} \ No newline at end of file diff --git a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt b/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt deleted file mode 100644 index a32d12ef23..0000000000 --- a/node-schemas/src/main/kotlin/net/corda/node/services/vault/schemas/requery/VaultSchema.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.corda.node.services.vault.schemas.requery - -import io.requery.* -import net.corda.core.node.services.Vault -import net.corda.core.schemas.requery.Requery -import java.time.Instant -import java.util.* - -object VaultSchema { - - @Table(name = "vault_transaction_notes") - @Entity(model = "vault") - interface VaultTxnNote : Persistable { - @get:Key - @get:Generated - @get:Column(name = "seq_no", index = true) - var seqNo: Int - - @get:Column(name = "transaction_id", length = 64, index = true) - var txId: String - - @get:Column(name = "note") - var note: String - } - - @Table(name = "vault_cash_balances") - @Entity(model = "vault") - interface VaultCashBalances : Persistable { - @get:Key - @get:Column(name = "currency_code", length = 3) - var currency: String - - @get:Column(name = "amount", value = "0") - var amount: Long - } - - @Table(name = "vault_states") - @Entity(model = "vault") - interface VaultStates : Requery.PersistentState { - /** refers to the notary a state is attached to */ - @get:Column(name = "notary_name") - var notaryName: String - - @get:Column(name = "notary_key", length = 65535) // TODO What is the upper limit on size of CompositeKey? - var notaryKey: String - - /** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */ - @get:Column(name = "contract_state_class_name") - var contractStateClassName: String - - /** refers to serialized transaction Contract State */ - // TODO: define contract state size maximum size and adjust length accordingly - @get:Column(name = "contract_state", length = 100000) - var contractState: ByteArray - - /** state lifecycle: unconsumed, consumed */ - @get:Column(name = "state_status") - var stateStatus: Vault.StateStatus - - /** refers to timestamp recorded upon entering UNCONSUMED state */ - @get:Column(name = "recorded_timestamp") - var recordedTime: Instant - - /** refers to timestamp recorded upon entering CONSUMED state */ - @get:Column(name = "consumed_timestamp", nullable = true) - var consumedTime: Instant? - - /** used to denote a state has been soft locked (to prevent double spend) - * will contain a temporary unique [UUID] obtained from a flow session */ - @get:Column(name = "lock_id", nullable = true) - var lockId: String? - - /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ - @get:Column(name = "lock_timestamp", nullable = true) - var lockUpdateTime: Instant? - } -} \ No newline at end of file diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt deleted file mode 100644 index 554353c29e..0000000000 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ /dev/null @@ -1,672 +0,0 @@ -package net.corda.node.services.vault.schemas - -import io.requery.Persistable -import io.requery.TransactionIsolation -import io.requery.kotlin.* -import io.requery.query.RowExpression -import io.requery.rx.KotlinRxEntityStore -import io.requery.sql.* -import io.requery.sql.platform.Generic -import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.composite.CompositeKey -import net.corda.core.crypto.generateKeyPair -import net.corda.core.crypto.toBase58String -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.node.services.Vault -import net.corda.core.schemas.requery.converters.InstantConverter -import net.corda.core.schemas.requery.converters.VaultStateStatusConverter -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize -import net.corda.core.transactions.LedgerTransaction -import net.corda.node.services.vault.schemas.requery.* -import net.corda.testing.ALICE -import net.corda.testing.BOB -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.TestDependencyInjectionBase -import net.corda.testing.contracts.DummyContract -import org.h2.jdbcx.JdbcDataSource -import org.junit.After -import org.junit.Assert -import org.junit.Before -import org.junit.Test -import rx.Observable -import java.time.Instant -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class VaultSchemaTest : TestDependencyInjectionBase() { - - var instance: KotlinEntityDataStore? = null - val data: KotlinEntityDataStore get() = instance!! - - var oinstance: KotlinRxEntityStore? = null - val odata: KotlinRxEntityStore get() = oinstance!! - - var transaction: LedgerTransaction? = null - - @Before - fun setup() { - val dataSource = JdbcDataSource() - dataSource.setURL("jdbc:h2:mem:vault_persistence;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1") - val configuration = KotlinConfiguration(dataSource = dataSource, model = Models.VAULT, mapping = setupCustomMapping(), useDefaultLogging = true) - instance = KotlinEntityDataStore(configuration) - oinstance = KotlinRxEntityStore(KotlinEntityDataStore(configuration)) - val tables = SchemaModifier(configuration) - val mode = TableCreationMode.DROP_CREATE - tables.createTables(mode) - - // create dummy test data - setupDummyData() - } - - private fun setupCustomMapping(): Mapping? { - val mapping = GenericMapping(Generic()) - val instantConverter = InstantConverter() - mapping.addConverter(instantConverter, instantConverter.mappedType) - val vaultStateStatusConverter = VaultStateStatusConverter() - mapping.addConverter(vaultStateStatusConverter, vaultStateStatusConverter.mappedType) - return mapping - } - - @After - fun tearDown() { - data.close() - } - - private class VaultNoopContract : Contract { - override val legalContractReference = SecureHash.sha256("") - - data class VaultNoopState(override val owner: AbstractParty) : OwnableState { - override val contract = VaultNoopContract() - override val participants: List - get() = listOf(owner) - - override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Commands.Create(), copy(owner = newOwner)) - } - - interface Commands : CommandData { - class Create : TypeOnlyCommandData(), Commands - } - - override fun verify(tx: LedgerTransaction) { - // Always accepts. - } - } - - private fun setupDummyData() { - // dummy Transaction comprised of 3 different Contract State types - // 1. SingleOwnerState - // 2. MultiOwnerState - // 3. VaultNoopState - val notary: Party = DUMMY_NOTARY - val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary) - val inState2 = TransactionState(DummyContract.MultiOwnerState(0, - listOf(ALICE, BOB)), notary) - val inState3 = TransactionState(VaultNoopContract.VaultNoopState(ALICE), notary) - val outState1 = inState1.copy() - val outState2 = inState2.copy() - val outState3 = inState3.copy() - val inputs = listOf(StateAndRef(inState1, StateRef(SecureHash.randomSHA256(), 0)), - StateAndRef(inState2, StateRef(SecureHash.randomSHA256(), 0)), - StateAndRef(inState3, StateRef(SecureHash.randomSHA256(), 0))) - val outputs = listOf(outState1, outState2, outState3) - val commands = emptyList>() - val attachments = emptyList() - val id = SecureHash.randomSHA256() - val timeWindow: TimeWindow? = null - val privacySalt: PrivacySalt = PrivacySalt() - transaction = LedgerTransaction( - inputs, - outputs, - commands, - attachments, - id, - notary, - timeWindow, - privacySalt - ) - } - - private fun createTxnWithTwoStateTypes(): LedgerTransaction { - val notary: Party = DUMMY_NOTARY - val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary) - val inState2 = TransactionState(DummyContract.MultiOwnerState(0, - listOf(ALICE, BOB)), notary) - val outState1 = inState1.copy() - val outState2 = inState2.copy() - val state1TxHash = SecureHash.randomSHA256() - val state2TxHash = SecureHash.randomSHA256() - val inputs = listOf(StateAndRef(inState1, StateRef(state1TxHash, 0)), - StateAndRef(inState1, StateRef(state1TxHash, 1)), - StateAndRef(inState2, StateRef(state2TxHash, 0)), - StateAndRef(inState1, StateRef(state1TxHash, 2))) // bogus state not in db - val outputs = listOf(outState1, outState2) - val commands = emptyList>() - val attachments = emptyList() - val id = SecureHash.randomSHA256() - val timeWindow: TimeWindow? = null - val privacySalt: PrivacySalt = PrivacySalt() - return LedgerTransaction( - inputs, - outputs, - commands, - attachments, - id, - notary, - timeWindow, - privacySalt - ) - } - - private fun dummyStatesInsert(txn: LedgerTransaction) { - data.invoke { - // skip inserting the last txn state (to mimic spend attempt of non existent unconsumed state) - txn.inputs.subList(0, txn.inputs.lastIndex).forEach { - insert(createStateEntity(it)) - // create additional state entities with idx >0 - for (i in 3..4) { - try { - createStateEntity(it, idx = i).apply { - insert(this) - } - } catch(e: Exception) { - } - } - // create additional state entities with different txn id - for (i in 1..3) { - createStateEntity(it, txHash = SecureHash.randomSHA256().toString()).apply { - insert(this) - } - } - } - // insert an additional MultiOwnerState with idx 1 - insert(createStateEntity(txn.inputs[2], idx = 1)) - - // insert entities with other state type - for (i in 1..5) { - VaultStatesEntity().apply { - txId = SecureHash.randomSHA256().toString() - index = 0 - contractStateClassName = VaultNoopContract.VaultNoopState::class.java.name - stateStatus = Vault.StateStatus.UNCONSUMED - insert(this) - } - } - } - - // check total numner of inserted states - assertEquals(3 + 4 + 9 + 1 + 5, data.select(VaultSchema.VaultStates::class).get().count()) - } - - /** - * Vault Schema: VaultStates - */ - @Test - fun testInsertState() { - val state = VaultStatesEntity() - state.txId = "12345" - state.index = 0 - data.invoke { - insert(state) - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq state.txId) - Assert.assertSame(state, result().first()) - } - } - - @Test - fun testUpsertUnconsumedState() { - val stateEntity = createStateEntity(transaction!!.inputs[0]) - data.invoke { - upsert(stateEntity) - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq stateEntity.txId) - Assert.assertSame(stateEntity, result().first()) - } - } - - @Test - fun testUpsertConsumedState() { - val stateEntity = createStateEntity(transaction!!.inputs[0]) - data.invoke { - upsert(stateEntity) - } - val keys = mapOf(VaultStatesEntity.TX_ID to stateEntity.txId, - VaultStatesEntity.INDEX to stateEntity.index) - val key = io.requery.proxy.CompositeKey(keys) - data.invoke { - val state = findByKey(VaultStatesEntity::class, key) - state?.run { - stateStatus = Vault.StateStatus.CONSUMED - consumedTime = Instant.now() - update(state) - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq state.txId) - assertEquals(Vault.StateStatus.CONSUMED, result().first().stateStatus) - } - } - } - - @Test - fun testCashBalanceUpdate() { - val cashBalanceEntity = VaultCashBalancesEntity() - cashBalanceEntity.currency = "USD" - cashBalanceEntity.amount = 100 - data.invoke { - val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency) - assertNull(state) - upsert(cashBalanceEntity) - } - data.invoke { - val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency) - state?.let { - state.amount -= 80 - upsert(state) - } - assertEquals(20, state!!.amount) - } - } - - @Test - fun testTransactionalUpsertState() { - data.withTransaction(TransactionIsolation.REPEATABLE_READ) { - transaction!!.inputs.forEach { - val stateEntity = createStateEntity(it) - insert(stateEntity) - } - val result = select(VaultSchema.VaultStates::class) - Assert.assertSame(3, result().toList().size) - } - data.invoke { - val result = select(VaultSchema.VaultStates::class) - Assert.assertSame(3, result().toList().size) - } - } - - @Test - fun testDistinctContractStateTypes() { - val txn = createTxnWithTwoStateTypes() - dummyStatesInsert(txn) - - data.invoke { - transaction!!.inputs.forEach { - val stateEntity = createStateEntity(it) - insert(stateEntity) - } - - val query = select(VaultSchema.VaultStates::contractStateClassName).distinct() - val results = query.get() - - Assert.assertSame(3, results.count()) - } - } - - private fun createStateEntity(stateAndRef: StateAndRef<*>, idx: Int? = null, txHash: String? = null): VaultStatesEntity { - val stateRef = stateAndRef.ref - val state = stateAndRef.state - return VaultStatesEntity().apply { - txId = txHash ?: stateRef.txhash.toString() - index = idx ?: stateRef.index - stateStatus = Vault.StateStatus.UNCONSUMED - contractStateClassName = state.data.javaClass.name - contractState = state.serialize().bytes - notaryName = state.notary.name.toString() - notaryKey = state.notary.owningKey.toBase58String() - recordedTime = Instant.now() - } - } - - /** - * Vault Schema: Transaction Notes - */ - @Test - fun testInsertTxnNote() { - val txnNoteEntity = VaultTxnNoteEntity() - txnNoteEntity.txId = "12345" - txnNoteEntity.note = "Sample transaction note" - data.invoke { - insert(txnNoteEntity) - val result = select(VaultSchema.VaultTxnNote::class) - Assert.assertSame(txnNoteEntity, result().first()) - } - } - - @Test - fun testFindTxnNote() { - val txnNoteEntity = VaultTxnNoteEntity() - txnNoteEntity.txId = "12345" - txnNoteEntity.note = "Sample transaction note #1" - val txnNoteEntity2 = VaultTxnNoteEntity() - txnNoteEntity2.txId = "23456" - txnNoteEntity2.note = "Sample transaction note #2" - data.invoke { - insert(txnNoteEntity) - insert(txnNoteEntity2) - } - data.invoke { - val result = select(VaultSchema.VaultTxnNote::class) where (VaultSchema.VaultTxnNote::txId eq txnNoteEntity2.txId) - assertEquals(result().count(), 1) - Assert.assertSame(txnNoteEntity2, result().first()) - } - } - - /** - * Vault Schema: Cash Balances - */ - @Test - fun testInsertCashBalance() { - val cashBalanceEntity = VaultCashBalancesEntity() - cashBalanceEntity.currency = "GPB" - cashBalanceEntity.amount = 12345 - data.invoke { - insert(cashBalanceEntity) - val result = select(VaultSchema.VaultCashBalances::class) - Assert.assertSame(cashBalanceEntity, result().first()) - } - } - - @Test - fun testUpdateCashBalance() { - val cashBalanceEntity = VaultCashBalancesEntity() - cashBalanceEntity.currency = "GPB" - cashBalanceEntity.amount = 12345 - data.invoke { - insert(cashBalanceEntity) - } - data.invoke { - val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency) - assertNotNull(state) - state?.let { - state.amount += 10000 - update(state) - val result = select(VaultCashBalancesEntity::class) - assertEquals(22345, result().first().amount) - } - } - } - - @Test - fun testUpsertCashBalance() { - val cashBalanceEntity = VaultCashBalancesEntity() - cashBalanceEntity.currency = "GPB" - cashBalanceEntity.amount = 12345 - data.invoke { - val state = findByKey(VaultCashBalancesEntity::class, cashBalanceEntity.currency) - state?.let { - state.amount += 10000 - } - val result = upsert(state ?: cashBalanceEntity) - assertEquals(12345, result.amount) - } - } - - @Test - fun testAllUnconsumedStates() { - data.invoke { - transaction!!.inputs.forEach { - insert(createStateEntity(it)) - } - } - val stateAndRefs = unconsumedStates() - assertNotNull(stateAndRefs) - assertTrue { stateAndRefs.size == 3 } - } - - @Test - fun tesUnconsumedDummyStates() { - data.invoke { - transaction!!.inputs.forEach { - insert(createStateEntity(it)) - } - } - val stateAndRefs = unconsumedStates() - assertNotNull(stateAndRefs) - assertTrue { stateAndRefs.size == 2 } - } - - @Test - fun tesUnconsumedDummySingleOwnerStates() { - data.invoke { - transaction!!.inputs.forEach { - insert(createStateEntity(it)) - } - } - val stateAndRefs = unconsumedStates() - assertNotNull(stateAndRefs) - assertTrue { stateAndRefs.size == 1 } - } - - inline fun unconsumedStates(): List> { - val stateAndRefs = - data.invoke { - val result = select(VaultSchema.VaultStates::class) - .where(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED) - result.get() - .map { it -> - val stateRef = StateRef(SecureHash.parse(it.txId), it.index) - val state = it.contractState.deserialize>() - StateAndRef(state, stateRef) - }.filter { - T::class.java.isAssignableFrom(it.state.data.javaClass) - }.toList() - } - return stateAndRefs - } - - /** - * Observables testing - */ - @Test - @Throws(Exception::class) - fun testInsert() { - val stateEntity = createStateEntity(transaction!!.inputs[0]) - val latch = CountDownLatch(1) - odata.insert(stateEntity).subscribe { - Assert.assertNotNull(it.txId) - Assert.assertTrue(it.txId.isNotEmpty()) - val cached = data.select(VaultSchema.VaultStates::class) - .where(VaultSchema.VaultStates::txId.eq(it.txId)).get().first() - Assert.assertSame(cached, it) - latch.countDown() - } - latch.await() - } - - @Test - @Throws(Exception::class) - fun testInsertCount() { - val stateEntity = createStateEntity(transaction!!.inputs[0]) - Observable.just(stateEntity) - .concatMap { person -> odata.insert(person).toObservable() } - odata.insert(stateEntity).toBlocking().value() - Assert.assertNotNull(stateEntity.txId) - Assert.assertTrue(stateEntity.txId.isNotEmpty()) - val count = data.count(VaultSchema.VaultStates::class).get().value() - Assert.assertEquals(1, count.toLong()) - } - - @Test - @Throws(Exception::class) - fun testQueryEmpty() { - val latch = CountDownLatch(1) - odata.select(VaultSchema.VaultStates::class).get().toObservable() - .subscribe({ Assert.fail() }, { Assert.fail() }) { latch.countDown() } - if (!latch.await(1, TimeUnit.SECONDS)) { - Assert.fail() - } - } - - @Test - @Throws(Exception::class) - fun testQueryObservable() { - transaction!!.inputs.forEach { - val stateEntity = createStateEntity(it) - odata.insert(stateEntity).toBlocking().value() - } - val states = ArrayList() - odata.select(VaultSchema.VaultStates::class).get() - .toObservable() - .subscribe { it -> states.add(it as VaultStatesEntity) } - Assert.assertEquals(3, states.size) - } - - /** - * Requery composite key tests (using RowExpression introduced in 1.2.1) - */ - @Test - fun testQueryWithCompositeKey() { - // txn entity with 4 input states (SingleOwnerState x 3, MultiOwnerState x 1) - val txn = createTxnWithTwoStateTypes() - dummyStatesInsert(txn) - - data.invoke { - val primaryCompositeKey = listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX) - val expression = RowExpression.of(primaryCompositeKey) - val stateRefs = txn.inputs.map { listOf("'${it.ref.txhash}'", it.ref.index) } - - val result = select(VaultStatesEntity::class) where (expression.`in`(stateRefs)) - assertEquals(3, result.get().count()) - } - } - - @Test - fun testUpdateWithCompositeKey() { - // txn entity with 4 input states (SingleOwnerState x 3, MultiOwnerState x 1) - val txn = createTxnWithTwoStateTypes() - dummyStatesInsert(txn) - - data.invoke { - val primaryCompositeKey = listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX) - val expression = RowExpression.of(primaryCompositeKey) - val stateRefs = txn.inputs.map { listOf("'${it.ref.txhash}'", it.ref.index) } - - val update = update(VaultStatesEntity::class) - .set(VaultStatesEntity.LOCK_ID, "") - .set(VaultStatesEntity.LOCK_UPDATE_TIME, Instant.now()) - .where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED) - .and(expression.`in`(stateRefs)).get() - assertEquals(3, update.value()) - } - } - - /** - * Soft locking tests - */ - @Test - fun testSingleSoftLockUpdate() { - - // insert unconsumed state - val stateEntity = createStateEntity(transaction!!.inputs[0]) - data.invoke { - upsert(stateEntity) - } - - // reserve soft lock on state - stateEntity.apply { - this.lockId = "LOCK#1" - this.lockUpdateTime = Instant.now() - data.invoke { - upsert(stateEntity) - } - } - - // select unlocked states - data.invoke { - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq stateEntity.txId) - .and(VaultSchema.VaultStates::lockId.isNull()) - assertEquals(0, result.get().count()) - } - - // release soft lock on state - data.invoke { - val update = update(VaultStatesEntity::class) - .set(VaultStatesEntity.LOCK_ID, null) - .set(VaultStatesEntity.LOCK_UPDATE_TIME, Instant.now()) - .where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED) - .and(VaultStatesEntity.LOCK_ID eq "LOCK#1").get() - assertEquals(1, update.value()) - } - - // select unlocked states - data.invoke { - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq stateEntity.txId) - .and(VaultSchema.VaultStates::lockId.isNull()) - assertEquals(1, result.get().count()) - } - } - - @Test - fun testMultipleSoftLocksUpdate() { - - // insert unconsumed state - data.withTransaction(TransactionIsolation.REPEATABLE_READ) { - transaction!!.inputs.forEach { - val stateEntity = createStateEntity(it) - insert(stateEntity) - } - val result = select(VaultSchema.VaultStates::class) - Assert.assertSame(3, result().toList().size) - } - - // reserve soft locks on states - transaction!!.inputs.forEach { - val stateEntity = createStateEntity(it) - stateEntity.apply { - this.lockId = "LOCK#1" - this.lockUpdateTime = Instant.now() - data.invoke { - upsert(stateEntity) - } - } - } - - // select unlocked states - val txnIds = transaction!!.inputs.map { it.ref.txhash.toString() }.toSet() - data.invoke { - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId `in` txnIds) - .and(VaultSchema.VaultStates::lockId eq "") - assertEquals(0, result.get().count()) - } - - // release soft lock on states - data.invoke { - val primaryCompositeKey = listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX) - val expression = RowExpression.of(primaryCompositeKey) - val stateRefs = transaction!!.inputs.map { listOf("'${it.ref.txhash}'", it.ref.index) } - - val update = update(VaultStatesEntity::class) - .set(VaultStatesEntity.LOCK_ID, "") - .set(VaultStatesEntity.LOCK_UPDATE_TIME, Instant.now()) - .where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED) - .and(expression.`in`(stateRefs)).get() - assertEquals(3, update.value()) - } - - // select unlocked states - data.invoke { - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId `in` txnIds) - .and(VaultSchema.VaultStates::lockId eq "") - assertEquals(3, result.get().count()) - } - } - - @Test - fun insertWithBigCompositeKey() { - val keys = (1..314).map { generateKeyPair().public } - val bigNotaryKey = CompositeKey.Builder().addKeys(keys).build() - val vaultStEntity = VaultStatesEntity().apply { - txId = SecureHash.randomSHA256().toString() - index = 314 - stateStatus = Vault.StateStatus.UNCONSUMED - contractStateClassName = VaultNoopContract.VaultNoopState::class.java.name - notaryName = "Huge distributed notary" - notaryKey = bigNotaryKey.toBase58String() - recordedTime = Instant.now() - } - data.insert(vaultStEntity) - assertEquals(1, data.select(VaultSchema.VaultStates::class).get().count()) - } -} \ No newline at end of file diff --git a/node/build.gradle b/node/build.gradle index 3fd0ffb452..9f4b2e7ccf 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -76,7 +76,6 @@ processSmokeTestResources { // build/reports/project/dependencies/index.html for green highlighted parts of the tree. dependencies { - compile project(':node-schemas') compile project(':node-api') compile project(':client:rpc') compile project(':cordform-common') @@ -145,10 +144,6 @@ dependencies { // For H2 database support in persistence compile "com.h2database:h2:$h2_version" - // Exposed: Kotlin SQL library - under evaluation - // TODO: Upgrade to Exposed 0.7 (has API changes) - compile "org.jetbrains.exposed:exposed:0.5.0" - // SQL connection pooling library compile "com.zaxxer:HikariCP:2.5.1" @@ -180,9 +175,6 @@ dependencies { compile 'commons-codec:commons-codec:1.10' compile 'com.github.bft-smart:library:master-v1.1-beta-g6215ec8-87' - // Requery: object mapper for Kotlin - compile "io.requery:requery-kotlin:$requery_version" - // FastClasspathScanner: classpath scanning compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21' @@ -200,7 +192,7 @@ dependencies { } task integrationTest(type: Test) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } @@ -211,7 +203,7 @@ task smokeTestJar(type: Jar) { task smokeTest(type: Test) { dependsOn smokeTestJar - testClassesDir = sourceSets.smokeTest.output.classesDir + testClassesDirs = sourceSets.smokeTest.output.classesDirs classpath = sourceSets.smokeTest.runtimeClasspath } diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index ae66f43dd7..0ce676a96b 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -26,8 +26,8 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { applicationSource = files( project(':node').configurations.runtime, project(':node').jar, - '../build/classes/main/CordaCaplet.class', - '../build/classes/main/CordaCaplet$1.class', + project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet.class', + project(':node').sourceSets.main.java.outputDir.toString() + '/CordaCaplet$1.class', "$rootDir/config/dev/log4j2.xml" ) from 'NOTICE' // Copy CDDL notice diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 00a64b0926..f1bb21c2d0 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -10,7 +10,7 @@ import net.corda.core.node.services.ServiceType import net.corda.core.utilities.getOrThrow import net.corda.testing.ALICE import net.corda.node.internal.NodeStartup -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.driver.ListenProcessDeathException import net.corda.testing.driver.NetworkMapStartStrategy diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt index 258c18cfae..2417f1d329 100644 --- a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -12,7 +12,7 @@ import net.corda.core.utilities.getOrThrow import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.core.utilities.unwrap -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.driver.driver import org.assertj.core.api.Assertions.assertThat diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt index b539dfee7a..d1f0a5e480 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -2,22 +2,22 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable import com.google.common.base.Stopwatch -import net.corda.core.contracts.DOLLARS import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow -import net.corda.core.utilities.minutes import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.OpaqueBytes -import net.corda.testing.performance.div -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow -import net.corda.node.services.startFlowPermission +import net.corda.core.utilities.minutes +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.driver.NodeHandle import net.corda.testing.driver.driver +import net.corda.testing.performance.div import net.corda.testing.performance.startPublishingFixedRateInjector import net.corda.testing.performance.startReporter import net.corda.testing.performance.startTightLoopInjector @@ -111,7 +111,7 @@ class NodePerformanceTests { a.rpcClientToNode().use("A", "A") { connection -> println("ISSUING") val doneFutures = (1..100).toList().parallelStream().map { - connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), a.nodeInfo.legalIdentity, a.nodeInfo.notaryIdentity).returnValue + connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), a.nodeInfo.notaryIdentity).returnValue }.toList() doneFutures.transpose().get() println("STARTING PAYMENT") diff --git a/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt index 9f422f7e16..204058c519 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/AdvertisedServiceTests.kt @@ -6,6 +6,7 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.nodeapi.User import net.corda.testing.driver.driver import org.bouncycastle.asn1.x500.X500Name diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index e9db0c5a85..26442c108c 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -1,7 +1,6 @@ package net.corda.node.services import net.corda.core.contracts.Amount -import net.corda.core.contracts.POUNDS import net.corda.core.identity.Party import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.messaging.CordaRPCOps @@ -10,10 +9,9 @@ import net.corda.core.messaging.startFlow import net.corda.core.node.NodeInfo import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.testing.ALICE -import net.corda.testing.DUMMY_NOTARY -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow +import net.corda.finance.POUNDS +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.* @@ -24,6 +22,7 @@ import org.junit.Test import rx.Observable import java.util.* import kotlin.test.assertEquals +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission class DistributedServiceTests : DriverBasedTest() { lateinit var alice: NodeHandle @@ -134,14 +133,10 @@ class DistributedServiceTests : DriverBasedTest() { } private fun issueCash(amount: Amount) { - val issueHandle = aliceProxy.startFlow( - ::CashIssueFlow, - amount, OpaqueBytes.of(0), alice.nodeInfo.legalIdentity, raftNotaryIdentity) - issueHandle.returnValue.getOrThrow() + aliceProxy.startFlow(::CashIssueFlow, amount, OpaqueBytes.of(0), raftNotaryIdentity).returnValue.getOrThrow() } private fun paySelf(amount: Amount) { - val payHandle = aliceProxy.startFlow(::CashPaymentFlow, amount, alice.nodeInfo.legalIdentity) - payHandle.returnValue.getOrThrow() + aliceProxy.startFlow(::CashPaymentFlow, amount, alice.nodeInfo.legalIdentity).returnValue.getOrThrow() } } diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/JDBCHashMapTestSuite.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/JDBCHashMapTestSuite.kt deleted file mode 100644 index 0057ba0470..0000000000 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/JDBCHashMapTestSuite.kt +++ /dev/null @@ -1,257 +0,0 @@ -package net.corda.node.utilities - -import com.google.common.collect.testing.MapTestSuiteBuilder -import com.google.common.collect.testing.SetTestSuiteBuilder -import com.google.common.collect.testing.TestStringMapGenerator -import com.google.common.collect.testing.TestStringSetGenerator -import com.google.common.collect.testing.features.CollectionFeature -import com.google.common.collect.testing.features.CollectionSize -import com.google.common.collect.testing.features.MapFeature -import com.google.common.collect.testing.features.SetFeature -import com.google.common.collect.testing.testers.* -import junit.framework.TestSuite -import net.corda.testing.* -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import org.assertj.core.api.Assertions.assertThat -import org.junit.* -import org.junit.runner.RunWith -import org.junit.runners.Suite -import java.util.* - -@RunWith(Suite::class) -@Suite.SuiteClasses( - JDBCHashMapTestSuite.MapLoadOnInitFalse::class, - JDBCHashMapTestSuite.MapLoadOnInitTrue::class, - JDBCHashMapTestSuite.MapConstrained::class, - JDBCHashMapTestSuite.SetLoadOnInitFalse::class, - JDBCHashMapTestSuite.SetLoadOnInitTrue::class, - JDBCHashMapTestSuite.SetConstrained::class) -class JDBCHashMapTestSuite { - companion object { - lateinit var transaction: DatabaseTransaction - lateinit var database: CordaPersistence - lateinit var loadOnInitFalseMap: JDBCHashMap - lateinit var memoryConstrainedMap: JDBCHashMap - lateinit var loadOnInitTrueMap: JDBCHashMap - lateinit var loadOnInitFalseSet: JDBCHashSet - lateinit var memoryConstrainedSet: JDBCHashSet - lateinit var loadOnInitTrueSet: JDBCHashSet - - @JvmStatic - @BeforeClass - fun before() { - initialiseTestSerialization() - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = { throw UnsupportedOperationException("Identity Service should not be in use") }) - setUpDatabaseTx() - loadOnInitFalseMap = JDBCHashMap("test_map_false", loadOnInit = false) - memoryConstrainedMap = JDBCHashMap("test_map_constrained", loadOnInit = false, maxBuckets = 1) - loadOnInitTrueMap = JDBCHashMap("test_map_true", loadOnInit = true) - loadOnInitFalseSet = JDBCHashSet("test_set_false", loadOnInit = false) - memoryConstrainedSet = JDBCHashSet("test_set_constrained", loadOnInit = false, maxBuckets = 1) - loadOnInitTrueSet = JDBCHashSet("test_set_true", loadOnInit = true) - } - - @JvmStatic - @AfterClass - fun after() { - closeDatabaseTx() - database.close() - resetTestSerialization() - } - - @JvmStatic - fun createMapTestSuite(loadOnInit: Boolean, constrained: Boolean): TestSuite = MapTestSuiteBuilder - .using(JDBCHashMapTestGenerator(loadOnInit = loadOnInit, constrained = constrained)) - .named("test JDBCHashMap with loadOnInit=$loadOnInit") - .withFeatures( - CollectionSize.ANY, - MapFeature.ALLOWS_ANY_NULL_QUERIES, - MapFeature.GENERAL_PURPOSE, - CollectionFeature.SUPPORTS_ITERATOR_REMOVE, - CollectionFeature.KNOWN_ORDER - ) - // putAll(null) not supported by Kotlin MutableMap interface - .suppressing(MapPutAllTester::class.java.getMethod("testPutAll_nullCollectionReference")) - // We suppress the following because of NotReallyMutableEntry - .suppressing(MapReplaceAllTester::class.java.getMethod("testReplaceAllPreservesOrder")) - .suppressing(MapReplaceAllTester::class.java.getMethod("testReplaceAllRotate")) - .suppressing(MapEntrySetTester::class.java.getMethod("testSetValue")) - .createTestSuite() - - @JvmStatic - fun createSetTestSuite(loadOnInit: Boolean, constrained: Boolean): TestSuite = SetTestSuiteBuilder - .using(JDBCHashSetTestGenerator(loadOnInit = loadOnInit, constrained = constrained)) - .named("test JDBCHashSet with loadOnInit=$loadOnInit") - .withFeatures( - CollectionSize.ANY, - SetFeature.GENERAL_PURPOSE, - CollectionFeature.SUPPORTS_ITERATOR_REMOVE, - CollectionFeature.KNOWN_ORDER - ) - // add/remove/retainAll(null) not supported by Kotlin MutableSet interface - .suppressing(CollectionAddAllTester::class.java.getMethod("testAddAll_nullCollectionReference")) - .suppressing(CollectionAddAllTester::class.java.getMethod("testAddAll_nullUnsupported")) - .suppressing(CollectionAddTester::class.java.getMethod("testAdd_nullUnsupported")) - .suppressing(CollectionCreationTester::class.java.getMethod("testCreateWithNull_unsupported")) - .suppressing(CollectionRemoveAllTester::class.java.getMethod("testRemoveAll_nullCollectionReferenceNonEmptySubject")) - .suppressing(CollectionRemoveAllTester::class.java.getMethod("testRemoveAll_nullCollectionReferenceEmptySubject")) - .suppressing(CollectionRetainAllTester::class.java.getMethod("testRetainAll_nullCollectionReferenceNonEmptySubject")) - .suppressing(CollectionRetainAllTester::class.java.getMethod("testRetainAll_nullCollectionReferenceEmptySubject")) - .createTestSuite() - - private fun setUpDatabaseTx() { - transaction = DatabaseTransactionManager.currentOrNew() - } - - private fun closeDatabaseTx() { - transaction.commit() - transaction.close() - } - } - - /** - * Guava test suite generator for JDBCHashMap(loadOnInit=false, constrained = false). - */ - class MapLoadOnInitFalse { - companion object { - @JvmStatic - fun suite(): TestSuite = createMapTestSuite(false, false) - } - } - - /** - * Guava test suite generator for JDBCHashMap(loadOnInit=false, constrained = true). - */ - class MapConstrained { - companion object { - @JvmStatic - fun suite(): TestSuite = createMapTestSuite(false, true) - } - } - - /** - * Guava test suite generator for JDBCHashMap(loadOnInit=true, constrained = false). - */ - class MapLoadOnInitTrue { - companion object { - @JvmStatic - fun suite(): TestSuite = createMapTestSuite(true, false) - } - } - - /** - * Generator of map instances needed for testing. - */ - class JDBCHashMapTestGenerator(val loadOnInit: Boolean, val constrained: Boolean) : TestStringMapGenerator() { - override fun create(elements: Array>): Map { - val map = if (loadOnInit) loadOnInitTrueMap else if (constrained) memoryConstrainedMap else loadOnInitFalseMap - map.clear() - map.putAll(elements.associate { Pair(it.key, it.value) }) - return map - } - } - - /** - * Guava test suite generator for JDBCHashSet(loadOnInit=false, constrained = false). - */ - class SetLoadOnInitFalse { - companion object { - @JvmStatic - fun suite(): TestSuite = createSetTestSuite(false, false) - } - } - - /** - * Guava test suite generator for JDBCHashSet(loadOnInit=false, constrained = true). - */ - class SetConstrained { - companion object { - @JvmStatic - fun suite(): TestSuite = createSetTestSuite(false, true) - } - } - - /** - * Guava test suite generator for JDBCHashSet(loadOnInit=true, constrained = false). - */ - class SetLoadOnInitTrue { - companion object { - @JvmStatic - fun suite(): TestSuite = createSetTestSuite(true, false) - } - } - - /** - * Generator of set instances needed for testing. - */ - class JDBCHashSetTestGenerator(val loadOnInit: Boolean, val constrained: Boolean) : TestStringSetGenerator() { - override fun create(elements: Array): Set { - val set = if (loadOnInit) loadOnInitTrueSet else if (constrained) memoryConstrainedSet else loadOnInitFalseSet - set.clear() - set.addAll(elements) - return set - } - } - - /** - * Test that the contents of a map can be reloaded from the database. - * - * If the Map reloads, then so will the Set as it just delegates. - */ - class MapCanBeReloaded : TestDependencyInjectionBase() { - private val ops = listOf(Triple(AddOrRemove.ADD, "A", "1"), - Triple(AddOrRemove.ADD, "B", "2"), - Triple(AddOrRemove.ADD, "C", "3"), - Triple(AddOrRemove.ADD, "D", "4"), - Triple(AddOrRemove.ADD, "E", "5"), - Triple(AddOrRemove.REMOVE, "A", "6"), - Triple(AddOrRemove.ADD, "G", "7"), - Triple(AddOrRemove.ADD, "H", "8"), - Triple(AddOrRemove.REMOVE, "D", "9"), - Triple(AddOrRemove.ADD, "C", "10")) - - private fun applyOpsToMap(map: MutableMap): MutableMap { - for (op in ops) { - if (op.first == AddOrRemove.ADD) { - map[op.second] = op.third - } else { - map.remove(op.second) - } - } - return map - } - - private val transientMapForComparison = applyOpsToMap(LinkedHashMap()) - - lateinit var database: CordaPersistence - - @Before - fun before() { - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = { throw UnsupportedOperationException("Identity Service should not be in use") }) - } - - @After - fun after() { - database.close() - } - - @Test - fun `fill map and check content after reconstruction`() { - database.transaction { - val persistentMap = JDBCHashMap("the_table") - // Populate map the first time. - applyOpsToMap(persistentMap) - assertThat(persistentMap.entries).containsExactly(*transientMapForComparison.entries.toTypedArray()) - } - database.transaction { - val persistentMap = JDBCHashMap("the_table", loadOnInit = false) - assertThat(persistentMap.entries).containsExactly(*transientMapForComparison.entries.toTypedArray()) - } - database.transaction { - val persistentMap = JDBCHashMap("the_table", loadOnInit = true) - assertThat(persistentMap.entries).containsExactly(*transientMapForComparison.entries.toTypedArray()) - } - } - } -} diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index df2cc5f069..7c96c484e4 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -14,7 +14,6 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.seconds import net.corda.node.internal.Node -import net.corda.node.services.api.DEFAULT_SESSION_ID import net.corda.node.services.messaging.* import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.services.transactions.SimpleNotaryService @@ -167,7 +166,7 @@ class P2PMessagingTest : NodeBasedTest() { distributedServiceNodes.forEach { val nodeName = it.info.legalIdentity.name var ignoreRequests = false - it.network.addMessageHandler(dummyTopic, DEFAULT_SESSION_ID) { netMessage, _ -> + it.network.addMessageHandler(dummyTopic) { netMessage, _ -> requestsReceived.incrementAndGet() firstRequestReceived.countDown() // The node which receives the first request will ignore all requests @@ -210,7 +209,7 @@ class P2PMessagingTest : NodeBasedTest() { } private fun Node.respondWith(message: Any) { - network.addMessageHandler(javaClass.name, DEFAULT_SESSION_ID) { netMessage, _ -> + network.addMessageHandler(javaClass.name) { netMessage, _ -> val request = netMessage.data.deserialize() val response = network.createMessage(javaClass.name, request.sessionID, message.serialize().bytes) network.send(response, request.replyTo) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2206df75ba..f75f050837 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1,7 +1,8 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting +import com.google.common.collect.Lists import com.google.common.collect.MutableClassToInstanceMap import com.google.common.util.concurrent.MoreExecutors import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner @@ -26,10 +27,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.debug import net.corda.core.utilities.toNonEmptySet -import net.corda.flows.CashExitFlow -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow -import net.corda.flows.IssuerFlow import net.corda.node.services.ContractUpgradeHandler import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotifyTransactionHandler @@ -67,7 +64,6 @@ import net.corda.node.utilities.* import net.corda.node.utilities.AddOrRemove.ADD import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import rx.Observable import java.io.IOException @@ -89,6 +85,9 @@ import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS import java.util.stream.Collectors.toList import kotlin.collections.ArrayList +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair @@ -210,9 +209,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, findRPCFlows(scanResult) } - // TODO Remove this once the cash stuff is in its own CorDapp - registerInitiatedFlow(IssuerFlow.Issuer::class.java) - runOnStop += network::stop _networkMapRegistrationFuture.captureLater(registerWithNetworkMapIfConfigured()) smm.start() @@ -384,11 +380,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, .filter { it.isUserInvokable() } + // Add any core flows here listOf( - ContractUpgradeFlow::class.java, - // TODO Remove all Cash flows from default list once they are split into separate CorDapp. - CashIssueFlow::class.java, - CashExitFlow::class.java, - CashPaymentFlow::class.java) + ContractUpgradeFlow::class.java) } /** @@ -420,9 +412,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeServices(): MutableList { checkpointStorage = DBCheckpointStorage() _services = ServiceHubInternalImpl() - attachments = NodeAttachmentService(configuration.dataSourceProperties, services.monitoringService.metrics, configuration.database) - network = makeMessagingService() - info = makeInfo() + attachments = NodeAttachmentService(services.monitoringService.metrics) + val legalIdentity = obtainIdentity("identity", configuration.myLegalName) + network = makeMessagingService(legalIdentity) + info = makeInfo(legalIdentity) val tokenizableServices = mutableListOf(attachments, network, services.vaultService, services.vaultQueryService, services.keyManagementService, services.identityService, platformClock, services.schedulerService) @@ -487,15 +480,14 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeVaultObservers() { VaultSoftLockManager(services.vaultService, smm) ScheduledActivityObserver(services) - HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties(), {services.identityService})) + HibernateObserver(services.vaultService.rawUpdates, services.database.hibernateConfig) } - private fun makeInfo(): NodeInfo { + private fun makeInfo(legalIdentity: PartyAndCertificate): NodeInfo { val advertisedServiceEntries = makeServiceEntries() - val legalIdentity = obtainLegalIdentity() - val allIdentitiesSet = (advertisedServiceEntries.map { it.identity } + legalIdentity).toNonEmptySet() + val allIdentities = (advertisedServiceEntries.map { it.identity } + legalIdentity).toNonEmptySet() val addresses = myAddresses() // TODO There is no support for multiple IP addresses yet. - return NodeInfo(addresses, legalIdentity, allIdentitiesSet, platformVersion, advertisedServiceEntries, findMyLocation()) + return NodeInfo(addresses, legalIdentity, allIdentities, platformVersion, advertisedServiceEntries, findMyLocation()) } /** @@ -506,7 +498,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return advertisedServices.map { val serviceId = it.type.id val serviceName = it.name ?: X500Name("${configuration.myLegalName},OU=$serviceId") - val identity = obtainKeyPair(serviceId, serviceName).first + val identity = obtainIdentity(serviceId, serviceName) ServiceEntry(it, identity) } } @@ -546,10 +538,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected open fun initialiseDatabasePersistence(insideTransaction: () -> Unit) { val props = configuration.dataSourceProperties if (props.isNotEmpty()) { - this.database = configureDatabase(props, configuration.database, identitySvc = { _services.identityService }) + this.database = configureDatabase(props, configuration.database, { _services.schemaService }, createIdentityService = { _services.identityService }) // Now log the vendor string as this will also cause a connection to be tested eagerly. database.transaction { - log.info("Connected to ${database.database.vendor} database.") + log.info("Connected to ${database.dataSource.connection.metaData.databaseProductName} database.") } this.database::close.let { dbCloser = it @@ -617,8 +609,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val instant = platformClock.instant() val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD val reg = NodeRegistration(info, instant.toEpochMilli(), ADD, expires) - val legalIdentityKey = obtainLegalIdentityKey() - val request = RegistrationRequest(reg.toWire(services.keyManagementService, legalIdentityKey.public), network.myAddress) + val request = RegistrationRequest(reg.toWire(services.keyManagementService, info.legalIdentityAndCert.owningKey), network.myAddress) return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } @@ -684,15 +675,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, runOnStop.clear() } - protected abstract fun makeMessagingService(): MessagingService + protected abstract fun makeMessagingService(legalIdentity: PartyAndCertificate): MessagingService protected abstract fun startMessagingService(rpcOps: RPCOps) - protected fun obtainLegalIdentity(): PartyAndCertificate = identityKeyPair.first - protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second - private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) } - - private fun obtainKeyPair(serviceId: String, serviceName: X500Name): Pair { + private fun obtainIdentity(id: String, name: X500Name): PartyAndCertificate { // Load the private identity key, creating it if necessary. The identity key is a long term well known key that // is distributed to other peers and we use it (or a key signed by it) when we need to do something // "permissioned". The identity file is what gets distributed and contains the node's legal name along with @@ -701,50 +688,52 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // TODO: Integrate with Key management service? val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) - val privateKeyAlias = "$serviceId-private-key" - val compositeKeyAlias = "$serviceId-composite-key" + val privateKeyAlias = "$id-private-key" + val compositeKeyAlias = "$id-composite-key" if (!keyStore.containsAlias(privateKeyAlias)) { val privKeyFile = configuration.baseDirectory / privateKeyAlias - val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" + val pubIdentityFile = configuration.baseDirectory / "$id-public" val compositeKeyFile = configuration.baseDirectory / compositeKeyAlias // TODO: Remove use of [ServiceIdentityGenerator.generateToDisk]. // Get keys from key file. // TODO: this is here to smooth out the key storage transition, remove this migration in future release. if (privKeyFile.exists()) { - migrateKeysFromFile(keyStore, serviceName, pubIdentityFile, privKeyFile, compositeKeyFile, privateKeyAlias, compositeKeyAlias) + migrateKeysFromFile(keyStore, name, pubIdentityFile, privKeyFile, compositeKeyFile, privateKeyAlias, compositeKeyAlias) } else { - log.info("$privateKeyAlias not found in keystore ${configuration.nodeKeystore}, generating fresh key!") - keyStore.saveNewKeyPair(serviceName, privateKeyAlias, generateKeyPair()) + log.info("$privateKeyAlias not found in key store ${configuration.nodeKeystore}, generating fresh key!") + keyStore.saveNewKeyPair(name, privateKeyAlias, generateKeyPair()) } } - val (cert, keys) = keyStore.certificateAndKeyPair(privateKeyAlias) - // Get keys from keystore. - val loadedServiceName = cert.subject - if (loadedServiceName != serviceName) - throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:$serviceName vs $loadedServiceName") + val (x509Cert, keys) = keyStore.certificateAndKeyPair(privateKeyAlias) - // Use composite key instead if exists // TODO: Use configuration to indicate composite key should be used instead of public key for the identity. - val (keyPair, certs) = if (keyStore.containsAlias(compositeKeyAlias)) { - val compositeKey = Crypto.toSupportedPublicKey(keyStore.getCertificate(compositeKeyAlias).publicKey) - val compositeKeyCert = keyStore.getCertificate(compositeKeyAlias) + val certificates = if (keyStore.containsAlias(compositeKeyAlias)) { + // Use composite key instead if it exists + val certificate = keyStore.getCertificate(compositeKeyAlias) // We have to create the certificate chain for the composite key manually, this is because in order to store - // the chain in keystore we need a private key, however there are no corresponding private key for composite key. - Pair(KeyPair(compositeKey, keys.private), listOf(compositeKeyCert, *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + // the chain in key store we need a private key, however there is no corresponding private key for the composite key. + Lists.asList(certificate, keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)) } else { - Pair(keys, keyStore.getCertificateChain(privateKeyAlias).toList()) + keyStore.getCertificateChain(privateKeyAlias).let { + check(it[0].toX509CertHolder() == x509Cert) { "Certificates from key store do not line up!" } + it.asList() + } } - val certPath = CertificateFactory.getInstance("X509").generateCertPath(certs) + + val subject = certificates[0].toX509CertHolder().subject + if (subject != name) + throw ConfigurationException("The name for $id doesn't match what's in the key store: $name vs $subject") + partyKeys += keys - return Pair(PartyAndCertificate(loadedServiceName, keyPair.public, X509CertificateHolder(certs.first().encoded), certPath), keyPair) + return PartyAndCertificate(CertificateFactory.getInstance("X509").generateCertPath(certificates)) } private fun migrateKeysFromFile(keyStore: KeyStoreWrapper, serviceName: X500Name, pubKeyFile: Path, privKeyFile: Path, compositeKeyFile:Path, privateKeyAlias: String, compositeKeyAlias: String) { - log.info("Migrating $privateKeyAlias from file to keystore...") + log.info("Migrating $privateKeyAlias from file to key store...") // Check that the identity in the config file matches the identity file we have stored to disk. // Load the private key. val publicKey = Crypto.decodePublicKey(pubKeyFile.readAll()) @@ -757,16 +746,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, log.info("Finish migrating $privateKeyAlias from file to keystore.") } - private fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair): PartyAndCertificate { - val certFactory = CertificateFactory.getInstance("X509") - val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) - val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) - return PartyAndCertificate(party, certHolder, certPath) - } - protected open fun generateKeyPair() = cryptoGenerateKeyPair() private inner class ServiceHubInternalImpl : ServiceHubInternal, SingletonSerializeAsToken() { + override val rpcFlows = ArrayList>>() override val stateMachineRecordedTransactionMapping = DBTransactionMappingStorage() override val auditService = DummyAuditService() @@ -774,9 +757,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val validatedTransactions = makeTransactionStorage() override val transactionVerifierService by lazy { makeTransactionVerifierService() } override val networkMapCache by lazy { InMemoryNetworkMapCache(this) } - override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties, configuration.database) } + override val vaultService by lazy { NodeVaultService(this) } override val vaultQueryService by lazy { - HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), { identityService }), vaultService.updatesPublisher) + HibernateVaultQueryImpl(database.hibernateConfig, vaultService) } // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with @@ -812,9 +795,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return flowFactories[initiatingFlowClass] } - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { database.transaction { - super.recordTransactions(txs) + super.recordTransactions(notifyVault, txs) } } override fun jdbcSession(): Connection = database.createSession() diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 8b604c0940..79ca9d2f22 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -21,7 +21,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.getRpcContext import net.corda.node.services.messaging.requirePermission -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.utilities.CordaPersistence diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index fbe201cb4f..6ca08f393b 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -2,6 +2,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import net.corda.core.concurrent.CordaFuture +import net.corda.core.identity.PartyAndCertificate import net.corda.core.internal.concurrent.doneFuture import net.corda.core.internal.concurrent.flatMap import net.corda.core.internal.concurrent.openFuture @@ -54,11 +55,10 @@ import kotlin.system.exitProcess * @param configuration This is typically loaded from a TypeSafe HOCON configuration file. * @param advertisedServices The services this node advertises. This must be a subset of the services it runs, * but nodes are not required to advertise services they run (hence subset). - * @param clock The clock used within the node and by all flows etc. */ open class Node(override val configuration: FullNodeConfiguration, advertisedServices: Set, - val versionInfo: VersionInfo, + private val versionInfo: VersionInfo, val initialiseSerialization: Boolean = true ) : AbstractNode(configuration, advertisedServices, createClock(configuration)) { companion object { @@ -127,13 +127,13 @@ open class Node(override val configuration: FullNodeConfiguration, // serialisation/deserialisation work. override val serverThread = AffinityExecutor.ServiceAffinityExecutor("Node thread", 1) - var messageBroker: ArtemisMessagingServer? = null + private var messageBroker: ArtemisMessagingServer? = null private var shutdownHook: ShutdownHook? = null private lateinit var userService: RPCUserService - override fun makeMessagingService(): MessagingService { + override fun makeMessagingService(legalIdentity: PartyAndCertificate): MessagingService { userService = RPCUserServiceImpl(configuration.rpcUsers) val (serverAddress, advertisedAddress) = with(configuration) { @@ -147,7 +147,7 @@ open class Node(override val configuration: FullNodeConfiguration, printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) - val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null + val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) legalIdentity.owningKey else null return NodeMessagingClient( configuration, versionInfo, @@ -340,7 +340,7 @@ open class Node(override val configuration: FullNodeConfiguration, private fun initialiseSerialization() { SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply { - registerScheme(KryoServerSerializationScheme(this)) + registerScheme(KryoServerSerializationScheme()) registerScheme(AMQPServerSerializationScheme()) } SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT diff --git a/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt index 14b5ef144e..fdaed4eebf 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/SerializationScheme.kt @@ -2,7 +2,6 @@ package net.corda.node.serialization import com.esotericsoftware.kryo.pool.KryoPool import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationFactory import net.corda.core.utilities.ByteSequence import net.corda.node.services.messaging.RpcServerObservableSerializer import net.corda.nodeapi.RPCKryo @@ -10,7 +9,7 @@ import net.corda.nodeapi.internal.serialization.AbstractKryoSerializationScheme import net.corda.nodeapi.internal.serialization.DefaultKryoCustomizer import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1 -class KryoServerSerializationScheme(serializationFactory: SerializationFactory) : AbstractKryoSerializationScheme(serializationFactory) { +class KryoServerSerializationScheme : AbstractKryoSerializationScheme() { override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean { return byteSequence == KryoHeaderV0_1 && target != SerializationContext.UseCase.RPCClient } @@ -21,7 +20,7 @@ class KryoServerSerializationScheme(serializationFactory: SerializationFactory) override fun rpcServerKryoPool(context: SerializationContext): KryoPool { return KryoPool.Builder { - DefaultKryoCustomizer.customize(RPCKryo(RpcServerObservableSerializer, serializationFactory, context)).apply { classLoader = context.deserializationClassLoader } + DefaultKryoCustomizer.customize(RPCKryo(RpcServerObservableSerializer, context)).apply { classLoader = context.deserializationClassLoader } }.build() } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt b/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt index d1a260a581..2c4ed57754 100644 --- a/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt +++ b/node/src/main/kotlin/net/corda/node/services/RPCUserService.kt @@ -9,6 +9,7 @@ import net.corda.nodeapi.User * to. These permissions are represented as [String]s to allow RPC implementations to add their own permissioning. */ interface RPCUserService { + fun getUser(username: String): User? val users: List } @@ -20,6 +21,35 @@ class RPCUserServiceImpl(override val users: List) : RPCUserService { override fun getUser(username: String): User? = users.find { it.username == username } } -fun startFlowPermission(className: String) = "StartFlow.$className" -fun

> startFlowPermission(clazz: Class

) = startFlowPermission(clazz.name) -inline fun > startFlowPermission(): String = startFlowPermission(P::class.java) +/** + * Helper class for creating flow class permissions. + */ +class FlowPermissions { + companion object { + + /** + * Creates the flow permission string of the format "StartFlow.{ClassName}". + * + * @param className a flow class name for which permission is created. + */ + @JvmStatic + fun startFlowPermission(className: String) = "StartFlow.$className" + + /** + * An overload for the [startFlowPermission] + * + * @param clazz a class for which permission is created. + * + */ + @JvmStatic + fun

> startFlowPermission(clazz: Class

) = startFlowPermission(clazz.name) + + /** + * An overload for the [startFlowPermission]. + * + * @param P a class for which permission is created. + */ + @JvmStatic + inline fun > startFlowPermission(): String = startFlowPermission(P::class.java) + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt index 73acab6e44..f14f0af01a 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt @@ -27,7 +27,7 @@ abstract class AbstractNodeService(val services: ServiceHubInternal) : Singleton addMessageHandler(topic: String, crossinline handler: (Q) -> R, crossinline exceptionConsumer: (Message, Exception) -> Unit): MessageHandlerRegistration { - return network.addMessageHandler(topic, DEFAULT_SESSION_ID) { message, _ -> + return network.addMessageHandler(topic, MessagingService.DEFAULT_SESSION_ID) { message, _ -> try { val request = message.data.deserialize() val response = handler(request) diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 2005010b83..69e010892b 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -1,6 +1,6 @@ package net.corda.node.services.api -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.concurrent.CordaFuture import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator @@ -24,12 +24,6 @@ import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.utilities.CordaPersistence -/** - * Session ID to use for services listening for the first message in a session (before a - * specific session ID has been established). - */ -val DEFAULT_SESSION_ID = 0L - interface NetworkMapCacheInternal : NetworkMapCache { /** * Deregister from updates from the given map service. @@ -89,8 +83,8 @@ interface ServiceHubInternal : PluginServiceHub { val database: CordaPersistence val configuration: NodeConfiguration - override fun recordTransactions(txs: Iterable) { - require (txs.any()) { "No transactions passed in for recording" } + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { + require(txs.any()) { "No transactions passed in for recording" } val recordedTransactions = txs.filter { validatedTransactions.addTransaction(it) } val stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id if (stateMachineRunId != null) { @@ -101,8 +95,10 @@ interface ServiceHubInternal : PluginServiceHub { log.warn("Transactions recorded from outside of a state machine") } - val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } - vaultService.notifyAll(toNotify) + if (notifyVault) { + val toNotify = recordedTransactions.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx } + vaultService.notifyAll(toNotify) + } } /** diff --git a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt index d0f300b9ee..03fe0aed2d 100644 --- a/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/database/HibernateConfiguration.kt @@ -7,10 +7,10 @@ import net.corda.core.schemas.converters.AbstractPartyToX500NameAsStringConverte import net.corda.core.utilities.loggerFor import net.corda.node.services.api.SchemaService import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.parserTransactionIsolationLevel import org.hibernate.SessionFactory import org.hibernate.boot.MetadataSources -import org.hibernate.boot.model.naming.Identifier -import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl +import org.hibernate.boot.model.naming.* import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder import org.hibernate.cfg.Configuration import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider @@ -20,37 +20,37 @@ import java.sql.Connection import java.util.* import java.util.concurrent.ConcurrentHashMap -class HibernateConfiguration(val schemaService: SchemaService, val databaseProperties: Properties, private val identitySvc: () -> IdentityService) { +class HibernateConfiguration(createSchemaService: () -> SchemaService, private val databaseProperties: Properties, private val createIdentityScervice: () -> IdentityService) { companion object { val logger = loggerFor() } // TODO: make this a guava cache or similar to limit ability for this to grow forever. - val sessionFactories = ConcurrentHashMap() + private val sessionFactories = ConcurrentHashMap, SessionFactory>() + + private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel") ?:"") + var schemaService = createSchemaService() init { - schemaService.schemaOptions.map { it.key }.forEach { mappedSchema -> - sessionFactories.computeIfAbsent(mappedSchema, { makeSessionFactoryForSchema(mappedSchema) }) - } + logger.info("Init HibernateConfiguration for schemas: ${schemaService.schemaOptions.keys}") + sessionFactoryForRegisteredSchemas() } fun sessionFactoryForRegisteredSchemas(): SessionFactory { - return sessionFactoryForSchemas(*schemaService.schemaOptions.map { it.key }.toTypedArray()) + return sessionFactoryForSchemas(*schemaService.schemaOptions.keys.toTypedArray()) } fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory { - return sessionFactories.computeIfAbsent(schema, { sessionFactoryForSchemas(schema) }) + return sessionFactoryForSchemas(schema) } + //vararg to set conversions left to preserve method signature for now fun sessionFactoryForSchemas(vararg schemas: MappedSchema): SessionFactory { - return makeSessionFactoryForSchemas(schemas.iterator()) + val schemaSet: Set = schemas.toSet() + return sessionFactories.computeIfAbsent(schemaSet, { makeSessionFactoryForSchemas(schemaSet) }) } - private fun makeSessionFactoryForSchema(schema: MappedSchema): SessionFactory { - return makeSessionFactoryForSchemas(setOf(schema).iterator()) - } - - private fun makeSessionFactoryForSchemas(schemas: Iterator): SessionFactory { + private fun makeSessionFactoryForSchemas(schemas: Set): SessionFactory { logger.info("Creating session factory for schemas: $schemas") val serviceRegistry = BootstrapServiceRegistryBuilder().build() val metadataSources = MetadataSources(serviceRegistry) @@ -60,12 +60,14 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name) .setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate") .setProperty("hibernate.format_sql", "true") + .setProperty("hibernate.connection.isolation", transactionIsolationLevel.toString()) schemas.forEach { schema -> // TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session schema.mappedTypes.forEach { config.addAnnotatedClass(it) } } - val sessionFactory = buildSessionFactory(config, metadataSources, "") + + val sessionFactory = buildSessionFactory(config, metadataSources, databaseProperties.getProperty("serverNameTablePrefix","")) logger.info("Created session factory for schemas: $schemas") return sessionFactory } @@ -80,7 +82,7 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope } }) // register custom converters - applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(identitySvc)) + applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(createIdentityScervice)) build() } diff --git a/node/src/main/kotlin/net/corda/node/services/database/KotlinConfigurationTransactionWrapper.kt b/node/src/main/kotlin/net/corda/node/services/database/KotlinConfigurationTransactionWrapper.kt deleted file mode 100644 index 78d4380161..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/database/KotlinConfigurationTransactionWrapper.kt +++ /dev/null @@ -1,146 +0,0 @@ -package net.corda.node.services.database - -import io.requery.EntityCache -import io.requery.TransactionIsolation -import io.requery.TransactionListener -import io.requery.cache.WeakEntityCache -import io.requery.meta.EntityModel -import io.requery.sql.* -import io.requery.sql.platform.H2 -import io.requery.util.function.Function -import io.requery.util.function.Supplier -import net.corda.core.schemas.requery.converters.InstantConverter -import net.corda.core.schemas.requery.converters.SecureHashConverter -import net.corda.core.schemas.requery.converters.StateRefConverter -import net.corda.core.schemas.requery.converters.VaultStateStatusConverter -import net.corda.node.utilities.DatabaseTransactionManager -import java.sql.Connection -import java.util.* -import java.util.concurrent.Executor -import javax.sql.DataSource - -/** - * Requery KotlinConfiguration wrapper class to enable us to pass in an existing database connection and - * associated transaction context. - */ -class KotlinConfigurationTransactionWrapper(private val model: EntityModel, - dataSource: DataSource, - private val mapping: Mapping? = null, - private val platform: Platform? = null, - private val cache: EntityCache = WeakEntityCache(), - private val useDefaultLogging: Boolean = false, - private val statementCacheSize: Int = 0, - private val batchUpdateSize: Int = 64, - private val quoteTableNames: Boolean = false, - private val quoteColumnNames: Boolean = false, - private val tableTransformer: Function? = null, - private val columnTransformer: Function? = null, - private val transactionMode: TransactionMode = TransactionMode.NONE, - private val transactionIsolation: TransactionIsolation? = null, - private val statementListeners: Set = LinkedHashSet(), - private val entityStateListeners: Set> = LinkedHashSet(), - private val transactionListeners: Set> = LinkedHashSet(), - private val writeExecutor: Executor? = null) : Configuration { - - private val connectionProvider = CordaDataSourceConnectionProvider(dataSource) - - override fun getBatchUpdateSize(): Int { - return batchUpdateSize - } - - override fun getConnectionProvider(): ConnectionProvider? { - return connectionProvider - } - - override fun getCache(): EntityCache? { - return cache - } - - override fun getEntityStateListeners(): Set> { - return entityStateListeners - } - - override fun getMapping(): Mapping? { - // TODO: database platform provider to become configurable and parameterised into this configuration - val customMapping = GenericMapping(H2()) - - // register our custom converters - val instantConverter = InstantConverter() - customMapping.addConverter(instantConverter, instantConverter.mappedType) - val vaultStateStatusConverter = VaultStateStatusConverter() - customMapping.addConverter(vaultStateStatusConverter, vaultStateStatusConverter.mappedType) - customMapping.addConverter(StateRefConverter(), StateRefConverter::getMappedType.javaClass) - customMapping.addConverter(SecureHashConverter(), SecureHashConverter::getMappedType.javaClass) - - return customMapping - } - - override fun getModel(): EntityModel { - return model - } - - override fun getPlatform(): Platform? { - return platform - } - - override fun getQuoteTableNames(): Boolean { - return quoteTableNames - } - - override fun getQuoteColumnNames(): Boolean { - return quoteColumnNames - } - - override fun getTableTransformer(): Function? { - return tableTransformer - } - - override fun getColumnTransformer(): Function? { - return columnTransformer - } - - override fun getStatementCacheSize(): Int { - return statementCacheSize - } - - override fun getStatementListeners(): Set? { - return statementListeners - } - - override fun getTransactionMode(): TransactionMode? { - return transactionMode - } - - override fun getTransactionIsolation(): TransactionIsolation? { - return transactionIsolation - } - - override fun getTransactionListenerFactories(): Set>? { - return transactionListeners - } - - override fun getUseDefaultLogging(): Boolean { - return useDefaultLogging - } - - override fun getWriteExecutor(): Executor? { - return writeExecutor - } - - class CordaDataSourceConnectionProvider(val dataSource: DataSource) : ConnectionProvider { - override fun getConnection(): Connection = CordaConnection(DatabaseTransactionManager.current().connection) - } - - class CordaConnection(val connection: Connection) : Connection by connection { - override fun close() { - // TODO: address requery auto-closing the connection in SchemaModifier upon table creation - // https://github.com/requery/requery/issues/424 - } - - override fun setAutoCommit(autoCommit: Boolean) { - // TODO: address requery bug in ConnectionTransaction commit() - // https://github.com/requery/requery/issues/423 - connection.autoCommit = false - } - } -} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/database/RequeryConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/database/RequeryConfiguration.kt deleted file mode 100644 index 8e2cc192a0..0000000000 --- a/node/src/main/kotlin/net/corda/node/services/database/RequeryConfiguration.kt +++ /dev/null @@ -1,65 +0,0 @@ -package net.corda.node.services.database - -import com.zaxxer.hikari.HikariConfig -import com.zaxxer.hikari.HikariDataSource -import io.requery.Persistable -import io.requery.TransactionIsolation -import io.requery.meta.EntityModel -import io.requery.sql.KotlinEntityDataStore -import io.requery.sql.SchemaModifier -import io.requery.sql.TableCreationMode -import net.corda.core.utilities.loggerFor -import net.corda.node.utilities.DatabaseTransactionManager -import java.sql.Connection -import java.util.* -import java.util.concurrent.ConcurrentHashMap - -class RequeryConfiguration(val properties: Properties, val useDefaultLogging: Boolean = false, val databaseProperties: Properties) { - - companion object { - val logger = loggerFor() - } - - // TODO: - // 1. schemaService schemaOptions needs to be applied: eg. default schema, table prefix - // 2. set other generic database configuration options: show_sql, format_sql - // 3. Configure Requery Database platform specific features (see http://requery.github.io/javadoc/io/requery/sql/Platform.html) - // 4. Configure Cache Manager and Cache Provider and set in Requery Configuration (see http://requery.github.io/javadoc/io/requery/EntityCache.html) - // 5. Consider database schema deployment/upgrade strategies to replace dynamic table creation. - - // Note: Annotations are pre-processed using (kapt) so no need to register dynamically - val config = HikariConfig(properties) - val dataSource = HikariDataSource(config) - - // TODO: make this a guava cache or similar to limit ability for this to grow forever. - private val sessionFactories = ConcurrentHashMap>() - - fun sessionForModel(model: EntityModel): KotlinEntityDataStore { - return sessionFactories.computeIfAbsent(model, { makeSessionFactoryForModel(it) }) - } - - fun makeSessionFactoryForModel(model: EntityModel): KotlinEntityDataStore { - val configuration = KotlinConfigurationTransactionWrapper(model, dataSource, useDefaultLogging = this.useDefaultLogging) - val tables = SchemaModifier(configuration) - if (databaseProperties.getProperty("initDatabase","true") == "true" ) { - val mode = TableCreationMode.CREATE_NOT_EXISTS - tables.createTables(mode) - } - return KotlinEntityDataStore(configuration) - } - - // TODO: remove once Requery supports QUERY WITH COMPOSITE_KEY IN - fun jdbcSession(): Connection = DatabaseTransactionManager.current().connection -} - -fun parserTransactionIsolationLevel(property: String?) : TransactionIsolation = - when (property) { - "none" -> TransactionIsolation.NONE - "readUncommitted" -> TransactionIsolation.READ_UNCOMMITTED - "readCommitted" -> TransactionIsolation.READ_COMMITTED - "repeatableRead" -> TransactionIsolation.REPEATABLE_READ - "serializable" -> TransactionIsolation.SERIALIZABLE - else -> { - TransactionIsolation.REPEATABLE_READ - } - } diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index e386a0d61b..908c20c399 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -1,13 +1,12 @@ package net.corda.node.services.events import com.google.common.util.concurrent.SettableFuture +import net.corda.core.contracts.* import net.corda.core.internal.ThreadBox -import net.corda.core.contracts.SchedulableState -import net.corda.core.contracts.ScheduledActivity -import net.corda.core.contracts.ScheduledStateRef -import net.corda.core.contracts.StateRef +import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic +import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace @@ -16,12 +15,12 @@ import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.utilities.* import org.apache.activemq.artemis.utils.ReusableLatch -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.statements.InsertStatement import java.time.Instant +import java.util.* import java.util.concurrent.Executor import java.util.concurrent.Executors import javax.annotation.concurrent.ThreadSafe +import javax.persistence.* /** * A first pass of a simple [SchedulerService] that works with [MutableClock]s for testing, demonstrations and simulations @@ -48,40 +47,44 @@ class NodeSchedulerService(private val services: ServiceHubInternal, companion object { private val log = loggerFor() + + fun createMap(): PersistentMap { + return PersistentMap( + toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, + fromPersistentEntity = { + //TODO null check will become obsolete after making DB/JPA columns not nullable + var txId = it.output.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") + var index = it.output.index ?: throw IllegalStateException("DB returned null SecureHash index") + Pair(StateRef(SecureHash.parse(txId), index), + ScheduledStateRef(StateRef(SecureHash.parse(txId), index), it.scheduledAt)) + }, + toPersistentEntity = { key: StateRef, value: ScheduledStateRef -> + PersistentScheduledState().apply { + output = PersistentStateRef(key.txhash.toString(), key.index) + scheduledAt = value.scheduledAt + } + }, + persistentEntityClass = PersistentScheduledState::class.java + ) + } } - private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}scheduled_states") { - val output = stateRef("transaction_id", "output_index") - val scheduledAt = instant("scheduled_at") - } + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}scheduled_states") + class PersistentScheduledState( + @EmbeddedId + var output: PersistentStateRef = PersistentStateRef(), + + @Column(name = "scheduled_at", nullable = false) + var scheduledAt: Instant = Instant.now() + ) - // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're - // inside mutex.locked {} code block. So we can't forget to take the lock unless we accidentally leak a reference - // to somewhere. private class InnerState { - var scheduledStates = object : AbstractJDBCHashMap(Table, loadOnInit = true) { - override fun keyFromRow(row: ResultRow): StateRef = StateRef(row[table.output.txId], row[table.output.index]) + var scheduledStates = createMap() - override fun valueFromRow(row: ResultRow): ScheduledStateRef { - return ScheduledStateRef(StateRef(row[table.output.txId], row[table.output.index]), row[table.scheduledAt]) - } + var scheduledStatesQueue: PriorityQueue = PriorityQueue( { a, b -> a.scheduledAt.compareTo(b.scheduledAt) } ) - override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { - insert[table.output.txId] = entry.key.txhash - insert[table.output.index] = entry.key.index - } - - override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { - insert[table.scheduledAt] = entry.value.scheduledAt - } - - } - var earliestState: ScheduledStateRef? = null var rescheduled: SettableFuture? = null - - internal fun recomputeEarliest() { - earliestState = scheduledStates.values.sortedBy { it.scheduledAt }.firstOrNull() - } } private val mutex = ThreadBox(InnerState()) @@ -89,7 +92,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, // We need the [StateMachineManager] to be constructed before this is called in case it schedules a flow. fun start() { mutex.locked { - recomputeEarliest() + scheduledStatesQueue.addAll(scheduledStates.all().map { it.second } .toMutableList()) rescheduleWakeUp() } } @@ -97,16 +100,20 @@ class NodeSchedulerService(private val services: ServiceHubInternal, override fun scheduleStateActivity(action: ScheduledStateRef) { log.trace { "Schedule $action" } mutex.locked { - if (scheduledStates.put(action.ref, action) == null) { + val previousState = scheduledStates[action.ref] + scheduledStates[action.ref] = action + var previousEarliest = scheduledStatesQueue.peek() + scheduledStatesQueue.remove(previousState) + scheduledStatesQueue.add(action) + if (previousState == null) { unfinishedSchedules.countUp() } - if (action.scheduledAt.isBefore(earliestState?.scheduledAt ?: Instant.MAX)) { + + if (action.scheduledAt.isBefore(previousEarliest?.scheduledAt ?: Instant.MAX)) { // We are earliest - earliestState = action rescheduleWakeUp() - } else if (earliestState?.ref == action.ref && earliestState!!.scheduledAt != action.scheduledAt) { + } else if(previousEarliest?.ref == action.ref && previousEarliest.scheduledAt != action.scheduledAt) { // We were earliest but might not be any more - recomputeEarliest() rescheduleWakeUp() } } @@ -117,9 +124,9 @@ class NodeSchedulerService(private val services: ServiceHubInternal, mutex.locked { val removedAction = scheduledStates.remove(ref) if (removedAction != null) { + scheduledStatesQueue.remove(removedAction) unfinishedSchedules.countDown() - if (removedAction == earliestState) { - recomputeEarliest() + if (removedAction == scheduledStatesQueue.peek()) { rescheduleWakeUp() } } @@ -139,7 +146,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, val (scheduledState, ourRescheduledFuture) = mutex.alreadyLocked { rescheduled?.cancel(false) rescheduled = SettableFuture.create() - Pair(earliestState, rescheduled!!) + Pair(scheduledStatesQueue.peek(), rescheduled!!) } if (scheduledState != null) { schedulerTimerExecutor.execute { @@ -157,7 +164,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, } private fun onTimeReached(scheduledState: ScheduledStateRef) { - serverThread.fetchFrom { + serverThread.execute { services.database.transaction { val scheduledFlow = getScheduledFlow(scheduledState) if (scheduledFlow != null) { @@ -175,28 +182,28 @@ class NodeSchedulerService(private val services: ServiceHubInternal, var scheduledFlow: FlowLogic<*>? = null mutex.locked { // need to remove us from those scheduled, but only if we are still next - scheduledStates.compute(scheduledState.ref) { _, value -> - if (value === scheduledState) { - if (scheduledActivity == null) { - log.info("Scheduled state $scheduledState has rescheduled to never.") - unfinishedSchedules.countDown() - null - } else if (scheduledActivity.scheduledAt.isAfter(services.clock.instant())) { - log.info("Scheduled state $scheduledState has rescheduled to ${scheduledActivity.scheduledAt}.") - ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) - } else { - // TODO: FlowLogicRefFactory needs to sort out the class loader etc - val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef) - log.trace { "Scheduler starting FlowLogic $flowLogic" } - scheduledFlow = flowLogic - null - } + val previousState = scheduledStates[scheduledState.ref] + if (previousState != null && previousState === scheduledState) { + if (scheduledActivity == null) { + log.info("Scheduled state $scheduledState has rescheduled to never.") + unfinishedSchedules.countDown() + scheduledStates.remove(scheduledState.ref) + scheduledStatesQueue.remove(scheduledState) + } else if (scheduledActivity.scheduledAt.isAfter(services.clock.instant())) { + log.info("Scheduled state $scheduledState has rescheduled to ${scheduledActivity.scheduledAt}.") + var newState = ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) + scheduledStates[scheduledState.ref] = newState + scheduledStatesQueue.remove(scheduledState) + scheduledStatesQueue.add(newState) } else { - value + val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef) + log.trace { "Scheduler starting FlowLogic $flowLogic" } + scheduledFlow = flowLogic + scheduledStates.remove(scheduledState.ref) + scheduledStatesQueue.remove(scheduledState) } } // and schedule the next one - recomputeEarliest() rescheduleWakeUp() } return scheduledFlow diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index a19ae3698a..80812edd1e 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -7,7 +7,9 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.toX509CertHolder import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace @@ -16,16 +18,13 @@ import org.bouncycastle.cert.X509CertificateHolder import java.security.InvalidAlgorithmParameterException import java.security.PublicKey import java.security.cert.* -import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.annotation.concurrent.ThreadSafe -import kotlin.collections.LinkedHashSet /** * Simple identity service which caches parties and provides functionality for efficient lookup. * * @param identities initial set of identities for the service, typically only used for unit tests. - * @param certPaths initial set of certificate paths for the service, typically only used for unit tests. */ @ThreadSafe class InMemoryIdentityService(identities: Iterable = emptySet(), @@ -43,7 +42,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS * Certificate store for certificate authority and intermediary certificates. */ override val caCertStore: CertStore - override val trustRootHolder = X509CertificateHolder(trustRoot.encoded) + override val trustRootHolder = trustRoot.toX509CertHolder() override val trustAnchor: TrustAnchor = TrustAnchor(trustRoot, null) private val keyToParties = ConcurrentHashMap() private val principalToParties = ConcurrentHashMap() @@ -54,7 +53,6 @@ class InMemoryIdentityService(identities: Iterable = emptyS keyToParties.putAll(identities.associateBy { it.owningKey } ) principalToParties.putAll(identities.associateBy { it.name }) confidentialIdentities.forEach { identity -> - require(identity.certPath.certificates.size >= 2) { "Certificate path must at least include subject and issuing certificates" } principalToParties.computeIfAbsent(identity.name) { identity } } } @@ -66,13 +64,10 @@ class InMemoryIdentityService(identities: Iterable = emptyS // TODO: Check the certificate validation logic @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) override fun verifyAndRegisterIdentity(identity: PartyAndCertificate): PartyAndCertificate? { - require(identity.certPath.certificates.size >= 2) { "Certificate path must at least include subject and issuing certificates" } // Validate the chain first, before we do anything clever with it identity.verify(trustAnchor) log.trace { "Registering identity $identity" } - require(Arrays.equals(identity.certificate.subjectPublicKeyInfo.encoded, identity.owningKey.encoded)) { "Party certificate must end with party's public key" } - keyToParties[identity.owningKey] = identity // Always keep the first party we registered, as that's the well known identity principalToParties.computeIfAbsent(identity.name) { identity } @@ -83,7 +78,7 @@ class InMemoryIdentityService(identities: Iterable = emptyS override fun certificateFromParty(party: Party): PartyAndCertificate = principalToParties[party.name] ?: throw IllegalArgumentException("Unknown identity ${party.name}") // We give the caller a copy of the data set to avoid any locking problems - override fun getAllIdentities(): Iterable = java.util.ArrayList(keyToParties.values) + override fun getAllIdentities(): Iterable = ArrayList(keyToParties.values) override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]?.party override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal]?.party @@ -128,13 +123,13 @@ class InMemoryIdentityService(identities: Iterable = emptyS return results } - @Throws(IdentityService.UnknownAnonymousPartyException::class) + @Throws(UnknownAnonymousPartyException::class) override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) { - val path = keyToParties[anonymousParty.owningKey]?.certPath ?: throw IdentityService.UnknownAnonymousPartyException("Unknown anonymous party ${anonymousParty.owningKey.toStringShort()}") - require(path.certificates.size > 1) { "Certificate path must contain at least two certificates" } - val actual = path.certificates[1] - require(actual is X509Certificate && actual.publicKey == party.owningKey) { "Next certificate in the path must match the party key ${party.owningKey.toStringShort()}." } - val target = path.certificates.first() - require(target is X509Certificate && target.publicKey == anonymousParty.owningKey) { "Certificate path starts with a certificate for the anonymous party" } + val anonymousIdentity = keyToParties[anonymousParty.owningKey] ?: + throw UnknownAnonymousPartyException("Unknown $anonymousParty") + val issuingCert = anonymousIdentity.certPath.certificates[1] + require(issuingCert.publicKey == party.owningKey) { + "Issuing certificate's public key must match the party key ${party.owningKey.toStringShort()}." + } } } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt index 07bf6b363b..36a2c4f34d 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -3,7 +3,6 @@ package net.corda.node.services.keys import net.corda.core.crypto.ContentSignerBuilder import net.corda.core.crypto.Crypto import net.corda.core.crypto.cert -import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService import net.corda.core.utilities.days @@ -35,10 +34,11 @@ fun freshCertificate(identityService: IdentityService, revocationEnabled: Boolean = false): PartyAndCertificate { val issuerCertificate = issuer.certificate val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCertificate) - val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) + val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, + issuerSigner, issuer.name, subjectPublicKey, window) val certFactory = CertificateFactory.getInstance("X509") val ourCertPath = certFactory.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) - val anonymisedIdentity = PartyAndCertificate(Party(issuer.name, subjectPublicKey), ourCertificate, ourCertPath) + val anonymisedIdentity = PartyAndCertificate(ourCertPath) identityService.verifyAndRegisterIdentity(anonymisedIdentity) return anonymisedIdentity } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index 919a744191..829c36bfbf 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -11,6 +11,7 @@ import net.corda.core.internal.ThreadBox import net.corda.core.internal.concurrent.openFuture import net.corda.core.internal.div import net.corda.core.internal.noneOrSingle +import net.corda.core.internal.toX509CertHolder import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange @@ -25,7 +26,6 @@ import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_TLS import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA -import net.corda.node.utilities.getX509Certificate import net.corda.node.utilities.loadKeyStore import net.corda.nodeapi.* import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER @@ -52,7 +52,6 @@ import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal import org.apache.activemq.artemis.utils.ConfigurationHelper import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.cert.X509CertificateHolder import rx.Subscription import java.io.IOException import java.math.BigInteger @@ -273,12 +272,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager { val keyStore = loadKeyStore(config.sslKeystore, config.keyStorePassword) val trustStore = loadKeyStore(config.trustStoreFile, config.trustStorePassword) - val ourCertificate = keyStore.getX509Certificate(CORDA_CLIENT_TLS) - // This is a sanity check and should not fail unless things have been misconfigured - require(ourCertificate.subject == config.myLegalName) { - "Legal name does not match with our subject CN: ${ourCertificate.subject}" - } val defaultCertPolicies = mapOf( PEER_ROLE to CertificateChainCheckPolicy.RootMustMatch, NODE_ROLE to CertificateChainCheckPolicy.LeafMustMatch, @@ -512,12 +506,12 @@ private class VerifyingNettyConnector(configuration: MutableMap, "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" } // Make sure certificate has the same name. - val peerCertificate = X509CertificateHolder(session.peerCertificateChain.first().encoded) + val peerCertificate = session.peerCertificateChain[0].toX509CertHolder() require(peerCertificate.subject == expectedLegalName) { "Peer has wrong subject name in the certificate - expected $expectedLegalName but got ${peerCertificate.subject}. This is either a fatal " + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" } - X509Utilities.validateCertificateChain(X509CertificateHolder(session.localCertificates.last().encoded), *session.peerCertificates) + X509Utilities.validateCertificateChain(session.localCertificates.last().toX509CertHolder(), *session.peerCertificates) server.onTcpConnection(peerLegalName) } catch (e: IllegalArgumentException) { connection.close() diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt index 3e65c572f0..4471cb24b2 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/Messaging.kt @@ -9,7 +9,6 @@ import net.corda.core.node.services.PartyInfo import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize -import net.corda.node.services.api.DEFAULT_SESSION_ID import org.bouncycastle.asn1.x500.X500Name import java.time.Instant import java.util.* @@ -28,6 +27,14 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe interface MessagingService { + companion object { + /** + * Session ID to use for services listening for the first message in a session (before a + * specific session ID has been established). + */ + val DEFAULT_SESSION_ID = 0L + } + /** * The provided function will be invoked for each received message whose topic matches the given string. The callback * will run on threads provided by the messaging service, and the callback is expected to be thread safe as a result. @@ -112,7 +119,7 @@ interface MessagingService { * @param sessionID identifier for the session the message is part of. For messages sent to services before the * construction of a session, use [DEFAULT_SESSION_ID]. */ -fun MessagingService.createMessage(topic: String, sessionID: Long = DEFAULT_SESSION_ID, data: ByteArray): Message +fun MessagingService.createMessage(topic: String, sessionID: Long = MessagingService.DEFAULT_SESSION_ID, data: ByteArray): Message = createMessage(TopicSession(topic, sessionID), data) /** @@ -191,8 +198,8 @@ interface MessageHandlerRegistration * a session is established, use [DEFAULT_SESSION_ID]. */ @CordaSerializable -data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) { - fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID +data class TopicSession(val topic: String, val sessionID: Long = MessagingService.DEFAULT_SESSION_ID) { + fun isBlank() = topic.isBlank() && sessionID == MessagingService.DEFAULT_SESSION_ID override fun toString(): String = "$topic.$sessionID" } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index c0ddd9f8e5..8bde1f046a 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -11,6 +11,9 @@ import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.TransactionVerifierService +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.* import net.corda.node.VersionInfo @@ -36,13 +39,12 @@ import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl import org.bouncycastle.asn1.x500.X500Name -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.statements.InsertStatement import java.security.PublicKey import java.time.Instant import java.util.* import java.util.concurrent.* import javax.annotation.concurrent.ThreadSafe +import javax.persistence.* // TODO: Stop the wallet explorer and other clients from using this class and get rid of persistentInbox @@ -68,12 +70,12 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe class NodeMessagingClient(override val config: NodeConfiguration, - val versionInfo: VersionInfo, - val serverAddress: NetworkHostAndPort, - val myIdentity: PublicKey?, - val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, + private val versionInfo: VersionInfo, + private val serverAddress: NetworkHostAndPort, + private val myIdentity: PublicKey?, + private val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, val database: CordaPersistence, - val networkMapRegistrationFuture: CordaFuture, + private val networkMapRegistrationFuture: CordaFuture, val monitoringService: MonitoringService, advertisedAddress: NetworkHostAndPort = serverAddress ) : ArtemisMessagingComponent(), MessagingService { @@ -93,6 +95,38 @@ class NodeMessagingClient(override val config: NodeConfiguration, private val verifierResponseAddress = "$VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX.${random63BitValue()}" private val messageMaxRetryCount: Int = 3 + + fun createProcessedMessage(): AppendOnlyPersistentMap { + return AppendOnlyPersistentMap( + toPersistentEntityKey = { it.toString() }, + fromPersistentEntity = { Pair(UUID.fromString(it.uuid), it.insertionTime) }, + toPersistentEntity = { key: UUID, value: Instant -> + ProcessedMessage().apply { + uuid = key.toString() + insertionTime = value + } + }, + persistentEntityClass = ProcessedMessage::class.java + ) + } + + fun createMessageToRedeliver(): PersistentMap, RetryMessage, Long> { + return PersistentMap( + toPersistentEntityKey = { it }, + fromPersistentEntity = { Pair(it.key, + Pair(it.message.deserialize( context = SerializationDefaults.STORAGE_CONTEXT), + it.recipients.deserialize( context = SerializationDefaults.STORAGE_CONTEXT)) + ) }, + toPersistentEntity = { _key: Long, (_message: Message, _recipient: MessageRecipients): Pair -> + RetryMessage().apply { + key = _key + message = _message.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes + recipients = _recipient.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes + } + }, + persistentEntityClass = RetryMessage::class.java + ) + } } private class InnerState { @@ -107,11 +141,11 @@ class NodeMessagingClient(override val config: NodeConfiguration, var verificationResponseConsumer: ClientConsumer? = null } - val messagesToRedeliver = database.transaction { - JDBCHashMap>("${NODE_DATABASE_PREFIX}message_retry", true) + private val messagesToRedeliver = database.transaction { + createMessageToRedeliver() } - val scheduledMessageRedeliveries = ConcurrentHashMap>() + private val scheduledMessageRedeliveries = ConcurrentHashMap>() val verifierService = when (config.verifierType) { VerifierType.InMemory -> InMemoryTransactionVerifierService(numberOfWorkers = 4) @@ -139,17 +173,33 @@ class NodeMessagingClient(override val config: NodeConfiguration, private val state = ThreadBox(InnerState()) private val handlers = CopyOnWriteArrayList() - private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}message_ids") { - val uuid = uuidString("message_id") - } + private val processedMessages = createProcessedMessage() - private val processedMessages: MutableSet = Collections.synchronizedSet( - object : AbstractJDBCHashSet(Table, loadOnInit = true) { - override fun elementFromRow(row: ResultRow): UUID = row[table.uuid] - override fun addElementToInsert(insert: InsertStatement, entry: UUID, finalizables: MutableList<() -> Unit>) { - insert[table.uuid] = entry - } - } + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}message_ids") + class ProcessedMessage( + @Id + @Column(name = "message_id", length = 36) + var uuid: String = "", + + @Column(name = "insertion_time") + var insertionTime: Instant = Instant.now() + ) + + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}message_retry") + class RetryMessage( + @Id + @Column(name = "message_id", length = 36) + var key: Long = 0, + + @Lob + @Column + var message: ByteArray = ByteArray(0), + + @Lob + @Column + var recipients: ByteArray = ByteArray(0) ) fun start(rpcOps: RPCOps, userService: RPCUserService) { @@ -374,7 +424,7 @@ class NodeMessagingClient(override val config: NodeConfiguration, callHandlers(msg, deliverTo) } // TODO We will at some point need to decide a trimming policy for the id's - processedMessages += msg.uniqueMessageId + processedMessages[msg.uniqueMessageId] = Instant.now() } } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ServiceRequestMessage.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ServiceRequestMessage.kt index 7536fcc33c..15c68a3b66 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ServiceRequestMessage.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ServiceRequestMessage.kt @@ -4,7 +4,6 @@ import net.corda.core.concurrent.CordaFuture import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.serialization.CordaSerializable -import net.corda.node.services.api.DEFAULT_SESSION_ID /** * Abstract superclass for request messages sent to services which expect a reply. @@ -23,6 +22,6 @@ fun MessagingService.sendRequest(topic: String, request: ServiceRequestMessage, target: MessageRecipients): CordaFuture { val responseFuture = onNext(topic, request.sessionID) - send(topic, DEFAULT_SESSION_ID, request, target) + send(topic, MessagingService.DEFAULT_SESSION_ID, request, target) return responseFuture } diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index 3af8f164e6..774e93f499 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -1,10 +1,10 @@ package net.corda.node.services.network -import com.google.common.annotations.VisibleForTesting import net.corda.core.concurrent.CordaFuture -import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.internal.VisibleForTesting +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.internal.concurrent.map import net.corda.core.internal.concurrent.openFuture import net.corda.core.messaging.DataFeed @@ -17,7 +17,6 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor -import net.corda.node.services.api.DEFAULT_SESSION_ID import net.corda.node.services.api.NetworkCacheError import net.corda.node.services.api.NetworkMapCacheInternal import net.corda.node.services.messaging.MessagingService @@ -99,11 +98,11 @@ open class InMemoryNetworkMapCache(private val serviceHub: ServiceHub?) : Single ifChangedSinceVer: Int?): CordaFuture { if (subscribe && !registeredForPush) { // Add handler to the network, for updates received from the remote network map service. - network.addMessageHandler(NetworkMapService.PUSH_TOPIC, DEFAULT_SESSION_ID) { message, _ -> + network.addMessageHandler(NetworkMapService.PUSH_TOPIC) { message, _ -> try { val req = message.data.deserialize() - val ackMessage = network.createMessage(NetworkMapService.PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, - NetworkMapService.UpdateAcknowledge(req.mapVersion, network.myAddress).serialize().bytes) + val ackMessage = network.createMessage(NetworkMapService.PUSH_ACK_TOPIC, + data = NetworkMapService.UpdateAcknowledge(req.mapVersion, network.myAddress).serialize().bytes) network.send(ackMessage, req.replyTo) processUpdatePush(req) } catch(e: NodeMapError) { diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index b5d5aa729b..21b728e6ff 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -1,12 +1,12 @@ package net.corda.node.services.network -import com.google.common.annotations.VisibleForTesting -import net.corda.core.internal.ThreadBox import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SignedData import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.random63BitValue import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.ThreadBox +import net.corda.core.internal.VisibleForTesting import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo @@ -20,9 +20,9 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.api.AbstractNodeService -import net.corda.node.services.api.DEFAULT_SESSION_ID import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.MessageHandlerRegistration +import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.ServiceRequestMessage import net.corda.node.services.messaging.createMessage import net.corda.node.services.network.NetworkMapService.* @@ -172,7 +172,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, handlers += addMessageHandler(QUERY_TOPIC) { req: QueryIdentityRequest -> processQueryRequest(req) } handlers += addMessageHandler(REGISTER_TOPIC) { req: RegistrationRequest -> processRegistrationRequest(req) } handlers += addMessageHandler(SUBSCRIPTION_TOPIC) { req: SubscribeRequest -> processSubscriptionRequest(req) } - handlers += network.addMessageHandler(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID) { message, _ -> + handlers += network.addMessageHandler(PUSH_ACK_TOPIC) { message, _ -> val req = message.data.deserialize() processAcknowledge(req) } @@ -290,7 +290,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, // to a MessageRecipientGroup that nodes join/leave, rather than the network map // service itself managing the group val update = NetworkMapService.Update(wireReg, newMapVersion, network.myAddress).serialize().bytes - val message = network.createMessage(PUSH_TOPIC, DEFAULT_SESSION_ID, update) + val message = network.createMessage(PUSH_TOPIC, data = update) subscribers.locked { // Remove any stale subscribers diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 1cb7de5292..2409d8a178 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -1,14 +1,19 @@ package net.corda.node.services.network -import net.corda.core.internal.ThreadBox +import net.corda.core.crypto.toBase58String import net.corda.core.identity.PartyAndCertificate +import net.corda.core.internal.ThreadBox import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.serialization.SerializationDefaults +import net.corda.core.serialization.deserialize +import net.corda.core.serialization.serialize import net.corda.node.services.api.ServiceHubInternal import net.corda.node.utilities.* -import org.bouncycastle.asn1.x500.X500Name -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.statements.InsertStatement -import java.util.Collections.synchronizedMap +import net.corda.nodeapi.ArtemisMessagingComponent +import java.io.ByteArrayInputStream +import java.security.cert.CertificateFactory +import javax.persistence.* +import java.util.* /** * A network map service backed by a database to survive restarts of the node hosting it. @@ -21,31 +26,100 @@ import java.util.Collections.synchronizedMap class PersistentNetworkMapService(services: ServiceHubInternal, minimumPlatformVersion: Int) : AbstractNetworkMapService(services, minimumPlatformVersion) { - private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}network_map_nodes") { - val nodeParty = partyAndCertificate("node_party_name", "node_party_key", "node_party_certificate", "node_party_path") - val registrationInfo = blob("node_registration_info") + // Only the node_party_path column is needed to reconstruct a PartyAndCertificate but we have the others for human readability + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}network_map_nodes") + class NetworkNode( + @Id @Column(name = "node_party_key") + var publicKey: String = "", + + @Column + var nodeParty: NodeParty = NodeParty(), + + @Lob @Column + var registrationInfo: ByteArray = ByteArray(0) + ) + + @Embeddable + class NodeParty( + @Column(name = "node_party_name") + var name: String = "", + + @Column(name = "node_party_certificate", length = 4096) + var certificate: ByteArray = ByteArray(0), + + @Column(name = "node_party_path", length = 4096) + var certPath: ByteArray = ByteArray(0) + ) + + private companion object { + private val factory = CertificateFactory.getInstance("X.509") + + fun createNetworkNodesMap(): PersistentMap { + return PersistentMap( + toPersistentEntityKey = { it.owningKey.toBase58String() }, + fromPersistentEntity = { + Pair(PartyAndCertificate(factory.generateCertPath(ByteArrayInputStream(it.nodeParty.certPath))), + it.registrationInfo.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + }, + toPersistentEntity = { key: PartyAndCertificate, value: NodeRegistrationInfo -> + NetworkNode( + publicKey = key.owningKey.toBase58String(), + nodeParty = NodeParty( + key.name.toString(), + key.certificate.encoded, + key.certPath.encoded + ), + registrationInfo = value.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes + ) + }, + persistentEntityClass = NetworkNode::class.java + ) + } + + fun createNetworkSubscribersMap(): PersistentMap { + return PersistentMap( + toPersistentEntityKey = { it.getPrimaryKeyBasedOnSubType() }, + fromPersistentEntity = { + Pair(it.key.deserialize(context = SerializationDefaults.STORAGE_CONTEXT), + it.value.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + }, + toPersistentEntity = { k: SingleMessageRecipient, v: LastAcknowledgeInfo -> + NetworkSubscriber( + id = k.getPrimaryKeyBasedOnSubType(), + key = k.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes, + value = v.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes + ) + }, + persistentEntityClass = NetworkSubscriber::class.java + ) + } + + fun SingleMessageRecipient.getPrimaryKeyBasedOnSubType() = + if (this is ArtemisMessagingComponent.ArtemisPeerAddress) { + this.hostAndPort.toString() + } else { + this.toString() + } } - override val nodeRegistrations: MutableMap = synchronizedMap(object : AbstractJDBCHashMap(Table, loadOnInit = true) { - // TODO: We should understand an X500Name database field type, rather than manually doing the conversion ourselves - override fun keyFromRow(row: ResultRow): PartyAndCertificate = PartyAndCertificate(X500Name(row[table.nodeParty.name]), row[table.nodeParty.owningKey], - row[table.nodeParty.certificate], row[table.nodeParty.certPath]) + override val nodeRegistrations: MutableMap = + Collections.synchronizedMap(createNetworkNodesMap()) - override fun valueFromRow(row: ResultRow): NodeRegistrationInfo = deserializeFromBlob(row[table.registrationInfo]) + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}network_map_subscribers") + class NetworkSubscriber( + @Id @Column + var id: String = "", - override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { - insert[table.nodeParty.name] = entry.key.name.toString() - insert[table.nodeParty.owningKey] = entry.key.owningKey - insert[table.nodeParty.certPath] = entry.key.certPath - insert[table.nodeParty.certificate] = entry.key.certificate - } + @Column(length = 4096) + var key: ByteArray = ByteArray(0), - override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { - insert[table.registrationInfo] = serializeToBlob(entry.value, finalizables) - } - }) + @Column(length = 4096) + var value: ByteArray = ByteArray(0) + ) - override val subscribers = ThreadBox(JDBCHashMap("${NODE_DATABASE_PREFIX}network_map_subscribers", loadOnInit = true)) + override val subscribers = ThreadBox(createNetworkSubscribersMap()) init { // Initialise the network map version with the current highest persisted version, or zero if there are no entries. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index b977f36875..a82642adf7 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -1,11 +1,10 @@ package net.corda.node.services.persistence -import net.corda.core.serialization.SerializationDefaults -import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize +import net.corda.core.serialization.SerializedBytes import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage -import net.corda.node.utilities.* +import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.NODE_DATABASE_PREFIX import javax.persistence.Column import javax.persistence.Entity import javax.persistence.Id @@ -28,11 +27,11 @@ class DBCheckpointStorage : CheckpointStorage { var checkpoint: ByteArray = ByteArray(0) ) - override fun addCheckpoint(value: Checkpoint) { + override fun addCheckpoint(checkpoint: Checkpoint) { val session = DatabaseTransactionManager.current().session session.save(DBCheckpoint().apply { - checkpointId = value.id.toString() - checkpoint = value.serialize(context = SerializationDefaults.CHECKPOINT_CONTEXT).bytes + checkpointId = checkpoint.id.toString() + this.checkpoint = checkpoint.serializedFiber.bytes }) } @@ -50,10 +49,9 @@ class DBCheckpointStorage : CheckpointStorage { val criteriaQuery = session.criteriaBuilder.createQuery(DBCheckpoint::class.java) val root = criteriaQuery.from(DBCheckpoint::class.java) criteriaQuery.select(root) - val query = session.createQuery(criteriaQuery) - val checkpoints = query.resultList.map { e -> e.checkpoint.deserialize(context = SerializationDefaults.CHECKPOINT_CONTEXT) }.asSequence() - for (e in checkpoints) { - if (!block(e)) { + for (row in session.createQuery(criteriaQuery).resultList) { + val checkpoint = Checkpoint(SerializedBytes(row.checkpoint)) + if (!block(checkpoint)) { break } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index fb637abd4c..0e68c98cce 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -1,6 +1,6 @@ package net.corda.node.services.persistence -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.crypto.SecureHash import net.corda.core.messaging.DataFeed diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 7dac06f5a0..28125f40ff 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -1,7 +1,7 @@ package net.corda.node.services.persistence import com.codahale.metrics.MetricRegistry -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream @@ -12,41 +12,50 @@ import net.corda.core.crypto.SecureHash import net.corda.core.node.services.AttachmentStorage import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor -import net.corda.node.services.database.RequeryConfiguration -import net.corda.node.services.persistence.schemas.requery.AttachmentEntity -import net.corda.node.services.persistence.schemas.requery.Models -import java.io.ByteArrayInputStream -import java.io.FilterInputStream -import java.io.IOException -import java.io.InputStream +import net.corda.node.utilities.DatabaseTransactionManager +import net.corda.node.utilities.NODE_DATABASE_PREFIX +import java.io.* import java.nio.file.FileAlreadyExistsException import java.nio.file.Paths -import java.util.* import java.util.jar.JarInputStream import javax.annotation.concurrent.ThreadSafe +import javax.persistence.* /** - * Stores attachments in H2 database. + * Stores attachments using Hibernate to database. */ @ThreadSafe -class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricRegistry, databaseProperties: Properties?) - : AttachmentStorage, SingletonSerializeAsToken() { +class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, SingletonSerializeAsToken() { + + @Entity + @Table(name = "${NODE_DATABASE_PREFIX}attachments", + indexes = arrayOf(Index(name = "att_id_idx", columnList = "att_id"))) + class DBAttachment( + @Id + @Column(name = "att_id", length = 65535) + var attId: String, + + @Column(name = "content") + @Lob + var content: ByteArray + ) : Serializable + companion object { private val log = loggerFor() } - val configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = databaseProperties ?: Properties()) - val session = configuration.sessionForModel(Models.PERSISTENCE) - @VisibleForTesting var checkAttachmentsOnLoad = true private val attachmentCount = metrics.counter("Attachments") init { - session.withTransaction { - attachmentCount.inc(session.count(AttachmentEntity::class).get().value().toLong()) - } + val session = DatabaseTransactionManager.current().session + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) + criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java))) + val count = session.createQuery(criteriaQuery).singleResult + attachmentCount.inc(count) } @CordaSerializable @@ -128,16 +137,13 @@ class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricReg } - override fun openAttachment(id: SecureHash): Attachment? = session.withTransaction { - try { - session.select(AttachmentEntity::class) - .where(AttachmentEntity.ATT_ID.eq(id)) - .get() - .single() - } catch (e: NoSuchElementException) { - null + override fun openAttachment(id: SecureHash): Attachment? { + val attachment = DatabaseTransactionManager.current().session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + attachment?.let { + return AttachmentImpl(id, { attachment.content }, checkAttachmentsOnLoad) } - }?.run { AttachmentImpl(id, { content }, checkAttachmentsOnLoad) } + return null + } // TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks. override fun importAttachment(jar: InputStream): SecureHash { @@ -153,25 +159,21 @@ class NodeAttachmentService(dataSourceProperties: Properties, metrics: MetricReg checkIsAValidJAR(ByteArrayInputStream(bytes)) val id = SecureHash.SHA256(hs.hash().asBytes()) - val count = session.withTransaction { - session.count(AttachmentEntity::class) - .where(AttachmentEntity.ATT_ID.eq(id)) - .get().value() - } - + val session = DatabaseTransactionManager.current().session + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(Long::class.java) + val attachments = criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java) + criteriaQuery.select(criteriaBuilder.count(criteriaQuery.from(NodeAttachmentService.DBAttachment::class.java))) + criteriaQuery.where(criteriaBuilder.equal(attachments.get(DBAttachment::attId.name), id.toString())) + val count = session.createQuery(criteriaQuery).singleResult if (count > 0) { throw FileAlreadyExistsException(id.toString()) } - session.withTransaction { - val attachment = AttachmentEntity() - attachment.attId = id - attachment.content = bytes - session.insert(attachment) - } + val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) + session.save(attachment) attachmentCount.inc() - log.info("Stored new attachment $id") return id diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index fc168e80b2..58fbe944a1 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -9,8 +9,8 @@ import net.corda.core.schemas.PersistentStateRef import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.database.HibernateConfiguration +import net.corda.node.utilities.DatabaseTransactionManager import org.hibernate.FlushMode -import org.jetbrains.exposed.sql.transactions.TransactionManager import rx.Observable /** @@ -40,7 +40,7 @@ class HibernateObserver(vaultUpdates: Observable>, v fun persistStateWithSchema(state: ContractState, stateRef: StateRef, schema: MappedSchema) { val sessionFactory = config.sessionFactoryForSchema(schema) val session = sessionFactory.withOptions(). - connection(TransactionManager.current().connection). + connection(DatabaseTransactionManager.current().connection). flushMode(FlushMode.MANUAL). openSession() session.use { diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 4c8e128fbb..331ef4461d 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -9,13 +9,18 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.services.api.SchemaService +import net.corda.node.services.events.NodeSchedulerService import net.corda.node.services.keys.PersistentKeyManagementService +import net.corda.node.services.messaging.NodeMessagingClient +import net.corda.node.services.network.PersistentNetworkMapService import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.persistence.DBTransactionMappingStorage import net.corda.node.services.persistence.DBTransactionStorage +import net.corda.node.services.persistence.NodeAttachmentService +import net.corda.node.services.transactions.BFTNonValidatingNotaryService import net.corda.node.services.transactions.PersistentUniquenessProvider +import net.corda.node.services.transactions.RaftUniquenessProvider import net.corda.node.services.vault.VaultSchemaV1 -import net.corda.schemas.CashSchemaV1 /** * Most basic implementation of [SchemaService]. @@ -35,18 +40,26 @@ class NodeSchemaService(customSchemas: Set = emptySet()) : SchemaS DBTransactionStorage.DBTransaction::class.java, DBTransactionMappingStorage.DBTransactionMapping::class.java, PersistentKeyManagementService.PersistentKey::class.java, - PersistentUniquenessProvider.PersistentUniqueness::class.java + PersistentUniquenessProvider.PersistentUniqueness::class.java, + PersistentUniquenessProvider.PersistentNotaryCommit::class.java, + NodeSchedulerService.PersistentScheduledState::class.java, + NodeAttachmentService.DBAttachment::class.java, + PersistentNetworkMapService.NetworkNode::class.java, + PersistentNetworkMapService.NetworkSubscriber::class.java, + NodeMessagingClient.ProcessedMessage::class.java, + NodeMessagingClient.RetryMessage::class.java, + NodeAttachmentService.DBAttachment::class.java, + RaftUniquenessProvider.RaftState::class.java, + BFTNonValidatingNotaryService.PersistedCommittedState::class.java )) // Required schemas are those used by internal Corda services // For example, cash is used by the vault for coin selection (but will be extracted as a standalone CorDapp in future) val requiredSchemas: Map = - mapOf(Pair(CashSchemaV1, SchemaService.SchemaOptions()), - Pair(CommonSchemaV1, SchemaService.SchemaOptions()), + mapOf(Pair(CommonSchemaV1, SchemaService.SchemaOptions()), Pair(VaultSchemaV1, SchemaService.SchemaOptions()), Pair(NodeServicesV1, SchemaService.SchemaOptions())) - override val schemaOptions: Map = requiredSchemas.plus(customSchemas.map { mappedSchema -> Pair(mappedSchema, SchemaService.SchemaOptions()) }) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index 150143cc34..6d16a94db5 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -1,6 +1,6 @@ package net.corda.node.services.statemachine -import com.google.common.annotations.VisibleForTesting +import net.corda.core.internal.VisibleForTesting import com.google.common.primitives.Primitives import net.corda.core.flows.* import net.corda.core.serialization.CordaSerializable diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStackSnapshotFactory.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStackSnapshotFactory.kt new file mode 100644 index 0000000000..72a7926eb2 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStackSnapshotFactory.kt @@ -0,0 +1,40 @@ +package net.corda.node.services.statemachine + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowStackSnapshot +import net.corda.core.flows.StateMachineRunId +import net.corda.core.utilities.loggerFor +import java.nio.file.Path +import java.util.* + +interface FlowStackSnapshotFactory { + private object Holder { + val INSTANCE: FlowStackSnapshotFactory + + init { + val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull() + INSTANCE = serviceFactory ?: DefaultFlowStackSnapshotFactory + } + } + + companion object { + val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE } + } + + fun getFlowStackSnapshot(flowClass: Class>): FlowStackSnapshot? + + fun persistAsJsonFile(flowClass: Class>, baseDir: Path, flowId: StateMachineRunId) + + private object DefaultFlowStackSnapshotFactory : FlowStackSnapshotFactory { + private val log = loggerFor() + + override fun getFlowStackSnapshot(flowClass: Class>): FlowStackSnapshot? { + log.warn("Flow stack snapshot are not supposed to be used in a production deployment") + return null + } + + override fun persistAsJsonFile(flowClass: Class>, baseDir: Path, flowId: StateMachineRunId) { + log.warn("Flow stack snapshot are not supposed to be used in a production deployment") + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 3820c0976f..977e1b416e 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -255,14 +255,12 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } @Suspendable - override fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? { - val factory = FlowStackSnapshotFactory.instance - return factory.getFlowStackSnapshot(flowClass) + override fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? { + return FlowStackSnapshotFactory.instance.getFlowStackSnapshot(flowClass) } - override fun persistFlowStackSnapshot(flowClass: Class<*>): Unit { - val factory = FlowStackSnapshotFactory.instance - factory.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id.toString()) + override fun persistFlowStackSnapshot(flowClass: Class>) { + FlowStackSnapshotFactory.instance.persistAsJsonFile(flowClass, serviceHub.configuration.baseDirectory, id) } /** diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index fbbbbaf4fd..4c2e3b0368 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -2,6 +2,7 @@ package net.corda.node.services.statemachine import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.FiberExecutorScheduler +import co.paralleluniverse.fibers.instrument.SuspendableHelper import co.paralleluniverse.strands.Strand import com.codahale.metrics.Gauge import com.esotericsoftware.kryo.KryoException @@ -166,11 +167,19 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val changes: Observable = mutex.content.changesPublisher.wrapWithDatabaseTransaction() fun start() { + checkQuasarJavaAgentPresence() restoreFibersFromCheckpoints() listenToLedgerTransactions() serviceHub.networkMapCache.mapServiceRegistered.then { executor.execute(this::resumeRestoredFibers) } } + private fun checkQuasarJavaAgentPresence() { + check(SuspendableHelper.isJavaAgentActive(), { + """Missing the '-javaagent' JVM argument. Make sure you run the tests with the Quasar java agent attached to your JVM. + #See https://docs.corda.net/troubleshooting.html - 'Fiber classes not instrumented' for more details.""".trimMargin("#") + }) + } + private fun listenToLedgerTransactions() { // Observe the stream of committed, validated transactions and resume fibers that are waiting for them. serviceHub.validatedTransactions.updates.subscribe { stx -> diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index b73868686d..feb4e83e55 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -2,15 +2,15 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.SettableFuture -import net.corda.core.crypto.Crypto -import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.SignableData -import net.corda.core.crypto.SignatureMetadata +import net.corda.core.contracts.StateRef +import net.corda.core.crypto.* import net.corda.core.flows.FlowLogic import net.corda.core.flows.NotaryException import net.corda.core.identity.Party import net.corda.core.node.services.NotaryService import net.corda.core.node.services.TimeWindowChecker +import net.corda.core.node.services.UniquenessProvider +import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.FilteredTransaction @@ -19,6 +19,10 @@ import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.core.utilities.unwrap import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.utilities.AppendOnlyPersistentMap +import net.corda.node.utilities.NODE_DATABASE_PREFIX +import org.bouncycastle.asn1.x500.X500Name +import javax.persistence.Entity import kotlin.concurrent.thread /** @@ -49,7 +53,7 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, c thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { configHandle.use { val timeWindowChecker = TimeWindowChecker(services.clock) - val replica = Replica(it, replicaId, "bft_smart_notary_committed_states", services, timeWindowChecker) + val replica = Replica(it, replicaId, { createMap() }, services, timeWindowChecker) replicaHolder.set(replica) log.info("BFT SMaRt replica $replicaId is running.") } @@ -88,11 +92,43 @@ class BFTNonValidatingNotaryService(override val services: ServiceHubInternal, c } } + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}bft_smart_notary_committed_states") + class PersistedCommittedState(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentUniquenessProvider.PersistentParty) + : PersistentUniquenessProvider.PersistentUniqueness(id, consumingTxHash, consumingIndex, party) + + fun createMap(): AppendOnlyPersistentMap = + AppendOnlyPersistentMap( + toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, + fromPersistentEntity = { + //TODO null check will become obsolete after making DB/JPA columns not nullable + var txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") + var index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index") + Pair(StateRef(txhash = SecureHash.parse(txId), index = index), + UniquenessProvider.ConsumingTx( + id = SecureHash.parse(it.consumingTxHash), + inputIndex = it.consumingIndex, + requestingParty = Party( + name = X500Name(it.party.name), + owningKey = parsePublicKeyBase58(it.party.owningKey)))) + }, + toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty): UniquenessProvider.ConsumingTx -> + PersistedCommittedState( + id = PersistentStateRef(txHash.toString(), index), + consumingTxHash = id.toString(), + consumingIndex = inputIndex, + party = PersistentUniquenessProvider.PersistentParty(requestingParty.name.toString(), + requestingParty.owningKey.toBase58String()) + ) + }, + persistentEntityClass = PersistedCommittedState::class.java + ) + private class Replica(config: BFTSMaRtConfig, replicaId: Int, - tableName: String, + createMap: () -> AppendOnlyPersistentMap, services: ServiceHubInternal, - timeWindowChecker: TimeWindowChecker) : BFTSMaRt.Replica(config, replicaId, tableName, services, timeWindowChecker) { + timeWindowChecker: TimeWindowChecker) : BFTSMaRt.Replica(config, replicaId, createMap, services, timeWindowChecker) { override fun executeCommand(command: ByteArray): ByteArray { val request = command.deserialize() diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index 8eafe93ea7..ba39a29689 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -22,6 +22,7 @@ import net.corda.core.internal.declaredField import net.corda.core.internal.toTypedArray import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.UniquenessProvider +import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize @@ -33,7 +34,7 @@ import net.corda.core.utilities.loggerFor import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.transactions.BFTSMaRt.Client import net.corda.node.services.transactions.BFTSMaRt.Replica -import net.corda.node.utilities.JDBCHashMap +import net.corda.node.utilities.AppendOnlyPersistentMap import java.nio.file.Path import java.util.* @@ -172,7 +173,8 @@ object BFTSMaRt { */ abstract class Replica(config: BFTSMaRtConfig, replicaId: Int, - tableName: String, + createMap: () -> AppendOnlyPersistentMap, protected val services: ServiceHubInternal, private val timeWindowChecker: TimeWindowChecker) : DefaultRecoverable() { companion object { @@ -191,9 +193,8 @@ object BFTSMaRt { } override fun getStateManager() = stateManagerOverride - // TODO: Use Requery with proper DB schema instead of JDBCHashMap. // Must be initialised before ServiceReplica is started - private val commitLog = services.database.transaction { JDBCHashMap(tableName) } + private val commitLog = services.database.transaction { createMap() } private val replica = run { config.waitUntilReplicaWillNotPrintStackTrace(replicaId) @Suppress("LeakingThis") @@ -229,7 +230,7 @@ object BFTSMaRt { log.debug { "No conflicts detected, committing input states: ${states.joinToString()}" } states.forEachIndexed { i, stateRef -> val txInfo = UniquenessProvider.ConsumingTx(txId, i, callerIdentity) - commitLog.put(stateRef, txInfo) + commitLog[stateRef] = txInfo } } else { log.debug { "Conflict detected – the following inputs have already been committed: ${conflicts.keys.joinToString()}" } @@ -261,7 +262,7 @@ object BFTSMaRt { // LinkedHashMap for deterministic serialisation val m = LinkedHashMap() services.database.transaction { - commitLog.forEach { m[it.key] = it.value } + commitLog.allPersisted().forEach { m[it.first] = it.second } } return m.serialize().bytes } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt b/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt index 6367346bbc..3688a2b493 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt @@ -8,9 +8,8 @@ import io.atomix.copycat.server.StateMachine import io.atomix.copycat.server.storage.snapshot.SnapshotReader import io.atomix.copycat.server.storage.snapshot.SnapshotWriter import net.corda.core.utilities.loggerFor -import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.JDBCHashMap -import java.util.* +import net.corda.node.utilities.* +import java.util.LinkedHashMap /** * A distributed map state machine that doesn't allow overriding values. The state machine is replicated @@ -20,9 +19,9 @@ import java.util.* * to disk, and sharing them across the cluster. A new node joining the cluster will have to obtain and install a snapshot * containing the entire JDBC table contents. */ -class DistributedImmutableMap(val db: CordaPersistence, tableName: String) : StateMachine(), Snapshottable { +class DistributedImmutableMap(val db: CordaPersistence, createMap: () -> AppendOnlyPersistentMap) : StateMachine(), Snapshottable { companion object { - private val log = loggerFor>() + private val log = loggerFor>() } object Commands { @@ -38,7 +37,7 @@ class DistributedImmutableMap(val db: CordaPersistence, tableN class Get(val key: K) : Query } - private val map = db.transaction { JDBCHashMap(tableName) } + private val map = db.transaction { createMap() } /** Gets a value for the given [Commands.Get.key] */ fun get(commit: Commit>): V? { @@ -80,7 +79,7 @@ class DistributedImmutableMap(val db: CordaPersistence, tableN override fun snapshot(writer: SnapshotWriter) { db.transaction { writer.writeInt(map.size) - map.entries.forEach { writer.writeObject(it.key to it.value) } + map.allPersisted().forEach { writer.writeObject(it.first to it.second) } } } @@ -92,7 +91,7 @@ class DistributedImmutableMap(val db: CordaPersistence, tableN // TODO: read & put entries in batches for (i in 1..size) { val (key, value) = reader.readObject>() - map.put(key, value) + map[key] = value } } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 820f3d5809..8c4d0f18e1 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -3,10 +3,12 @@ package net.corda.node.services.transactions import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.crypto.parsePublicKeyBase58 +import net.corda.core.crypto.toBase58String import net.corda.core.identity.Party import net.corda.core.internal.ThreadBox import net.corda.core.node.services.UniquenessException import net.corda.core.node.services.UniquenessProvider +import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.node.utilities.* @@ -20,41 +22,35 @@ import javax.persistence.* @ThreadSafe class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsToken() { - @Entity - @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_commit_log") - class PersistentUniqueness ( + @MappedSuperclass + open class PersistentUniqueness ( + @EmbeddedId + var id: PersistentStateRef = PersistentStateRef(), - @EmbeddedId - var id: StateRef = StateRef(), + @Column(name = "consuming_transaction_id") + var consumingTxHash: String = "", - @Column(name = "consuming_transaction_id") - var consumingTxHash: String = "", + @Column(name = "consuming_input_index", length = 36) + var consumingIndex: Int = 0, - @Column(name = "consuming_input_index", length = 36) - var consumingIndex: Int = 0, + @Embedded + var party: PersistentParty = PersistentParty() + ) - @Embedded - var party: Party = Party() - ) { + @Embeddable + data class PersistentParty( + @Column(name = "requesting_party_name") + var name: String = "", - @Embeddable - data class StateRef ( - @Column(name = "transaction_id") - var txId: String = "", + @Column(name = "requesting_party_key", length = 255) + var owningKey: String = "" + ): Serializable - @Column(name = "output_index", length = 36) - var index: Int = 0 - ) : Serializable + @Entity + @javax.persistence.Table(name = "${NODE_DATABASE_PREFIX}notary_commit_log") + class PersistentNotaryCommit(id: PersistentStateRef, consumingTxHash: String, consumingIndex: Int, party: PersistentParty): + PersistentUniqueness(id, consumingTxHash, consumingIndex, party) - @Embeddable - data class Party ( - @Column(name = "requesting_party_name") - var name: String = "", - - @Column(name = "requesting_party_key", length = 255) - var owningKey: String = "" - ) : Serializable - } private class InnerState { val committedStates = createMap() @@ -65,26 +61,32 @@ class PersistentUniquenessProvider : UniquenessProvider, SingletonSerializeAsTok companion object { private val log = loggerFor() - fun createMap(): AppendOnlyPersistentMap { - return AppendOnlyPersistentMap( - toPersistentEntityKey = { PersistentUniqueness.StateRef(it.txhash.toString(), it.index) }, - fromPersistentEntity = { - Pair(StateRef(SecureHash.parse(it.id.txId), it.id.index), - UniquenessProvider.ConsumingTx(SecureHash.parse(it.consumingTxHash), it.consumingIndex, - Party(X500Name(it.party.name), parsePublicKeyBase58(it.party.owningKey)))) + fun createMap(): AppendOnlyPersistentMap = + AppendOnlyPersistentMap( + toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) }, + fromPersistentEntity = { + //TODO null check will become obsolete after making DB/JPA columns not nullable + var txId = it.id.txId ?: throw IllegalStateException("DB returned null SecureHash transactionId") + var index = it.id.index ?: throw IllegalStateException("DB returned null SecureHash index") + Pair(StateRef(txhash = SecureHash.parse(txId), index = index), + UniquenessProvider.ConsumingTx( + id = SecureHash.parse(it.consumingTxHash), + inputIndex = it.consumingIndex, + requestingParty = Party( + name = X500Name(it.party.name), + owningKey = parsePublicKeyBase58(it.party.owningKey)))) + }, + toPersistentEntity = { (txHash, index) : StateRef, (id, inputIndex, requestingParty) : UniquenessProvider.ConsumingTx -> + PersistentNotaryCommit( + id = PersistentStateRef(txHash.toString(), index), + consumingTxHash = id.toString(), + consumingIndex = inputIndex, + party = PersistentParty(requestingParty.name.toString(), requestingParty.owningKey.toBase58String()) + ) }, - toPersistentEntity = { key: StateRef, value: UniquenessProvider.ConsumingTx -> - PersistentUniqueness().apply { - id = PersistentUniqueness.StateRef(key.txhash.toString(), key.index) - consumingTxHash = value.id.toString() - consumingIndex = value.inputIndex - party = PersistentUniqueness.Party(value.requestingParty.name.toString()) - } - }, - persistentEntityClass = PersistentUniqueness::class.java + persistentEntityClass = PersistentNotaryCommit::class.java ) } - } override fun commit(states: List, txId: SecureHash, callerIdentity: Party) { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index d7024866ad..69254beeee 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -10,6 +10,7 @@ import io.atomix.catalyst.transport.netty.NettyTransport import io.atomix.catalyst.transport.netty.SslProtocol import io.atomix.copycat.client.ConnectionStrategies import io.atomix.copycat.client.CopycatClient +import io.atomix.copycat.client.RecoveryStrategies import io.atomix.copycat.server.CopycatServer import io.atomix.copycat.server.storage.Storage import io.atomix.copycat.server.storage.StorageLevel @@ -18,16 +19,22 @@ import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.node.services.UniquenessException import net.corda.core.node.services.UniquenessProvider +import net.corda.core.serialization.SerializationDefaults import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.utilities.loggerFor import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.utilities.AppendOnlyPersistentMap import net.corda.node.utilities.CordaPersistence import net.corda.nodeapi.config.SSLConfiguration import java.nio.file.Path import java.util.concurrent.CompletableFuture import javax.annotation.concurrent.ThreadSafe +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Id +import javax.persistence.Lob /** * A uniqueness provider that records committed input states in a distributed collection replicated and @@ -41,9 +48,35 @@ import javax.annotation.concurrent.ThreadSafe class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, SingletonSerializeAsToken() { companion object { private val log = loggerFor() - private val DB_TABLE_NAME = "notary_committed_states" + + fun createMap(): AppendOnlyPersistentMap = + AppendOnlyPersistentMap( + toPersistentEntityKey = { it }, + fromPersistentEntity = { + Pair(it.key, it.value.deserialize(context = SerializationDefaults.STORAGE_CONTEXT)) + }, + toPersistentEntity = { k: String, v: Any -> + RaftState().apply { + key = k + value = v.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes + } + }, + persistentEntityClass = RaftState::class.java + ) } + @Entity + @javax.persistence.Table(name = "notary_committed_states") + class RaftState( + @Id + @Column + var key: String = "", + + @Lob + @Column + var value: ByteArray = ByteArray(0) + ) + /** Directory storing the Raft log and state machine snapshots */ private val storagePath: Path = services.configuration.baseDirectory /** Address of the Copycat node run by this Corda node */ @@ -70,7 +103,8 @@ class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, fun start() { log.info("Creating Copycat server, log stored in: ${storagePath.toFile()}") - val stateMachineFactory = { DistributedImmutableMap(db, DB_TABLE_NAME) } + val stateMachineFactory = { + DistributedImmutableMap(db, RaftUniquenessProvider.Companion::createMap) } val address = Address(myAddress.host, myAddress.port) val storage = buildStorage(storagePath) val transport = buildTransport(transportConfiguration) @@ -118,6 +152,7 @@ class RaftUniquenessProvider(services: ServiceHubInternal) : UniquenessProvider, .withTransport(transport) // TODO: use local transport for client-server communications .withConnectionStrategy(ConnectionStrategies.EXPONENTIAL_BACKOFF) .withSerializer(serializer) + .withRecoveryStrategy(RecoveryStrategies.RECOVER) .build() _clientFuture = serverFuture.thenCompose { client.connect(address) } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 2ee2218160..58a97f51e2 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -7,21 +7,22 @@ import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria -import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toHexString import net.corda.core.utilities.trace -import org.bouncycastle.asn1.x500.X500Name +import org.hibernate.query.criteria.internal.expression.LiteralExpression +import org.hibernate.query.criteria.internal.predicate.ComparisonPredicate +import org.hibernate.query.criteria.internal.predicate.InPredicate import java.util.* import javax.persistence.Tuple import javax.persistence.criteria.* class HibernateQueryCriteriaParser(val contractType: Class, - val contractTypeMappings: Map>, + val contractTypeMappings: Map>, val criteriaBuilder: CriteriaBuilder, val criteriaQuery: CriteriaQuery, val vaultStates: Root) : IQueryCriteriaParser { @@ -32,8 +33,9 @@ class HibernateQueryCriteriaParser(val contractType: Class, // incrementally build list of join predicates private val joinPredicates = mutableListOf() // incrementally build list of root entities (for later use in Sort parsing) - private val rootEntities = mutableMapOf, Root<*>>() + private val rootEntities = mutableMapOf, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() + private val commonPredicates = mutableMapOf, Predicate>() // schema attribute Name, operator -> predicate var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED @@ -41,11 +43,6 @@ class HibernateQueryCriteriaParser(val contractType: Class, log.trace { "Parsing VaultQueryCriteria: $criteria" } val predicateSet = mutableSetOf() - // contract State Types - val contractTypes = deriveContractTypes(criteria.contractStateTypes) - if (contractTypes.isNotEmpty()) - predicateSet.add(criteriaBuilder.and(vaultStates.get("contractStateClassName").`in`(contractTypes))) - // soft locking criteria.softLockingCondition?.let { val softLocking = criteria.softLockingCondition @@ -68,9 +65,8 @@ class HibernateQueryCriteriaParser(val contractType: Class, } // notary names - criteria.notaryName?.let { - val notaryNames = (criteria.notaryName as List).map { it.toString() } - predicateSet.add(criteriaBuilder.and(vaultStates.get("notaryName").`in`(notaryNames))) + criteria.notary?.let { + predicateSet.add(criteriaBuilder.and(vaultStates.get("notary").`in`(criteria.notary))) } // state references @@ -94,12 +90,14 @@ class HibernateQueryCriteriaParser(val contractType: Class, return predicateSet } - private fun deriveContractTypes(contractStateTypes: Set>? = null): List { + private fun deriveContractTypes(contractStateTypes: Set>? = null): Set { + log.trace { "Contract types to be derived: primary ($contractType), additional ($contractStateTypes)" } val combinedContractStateTypes = contractStateTypes?.plus(contractType) ?: setOf(contractType) combinedContractStateTypes.filter { it.name != ContractState::class.java.name }.let { - val interfaces = it.flatMap { contractTypeMappings[it.name] ?: emptyList() } + val interfaces = it.flatMap { contractTypeMappings[it.name] ?: setOf(it.name) } val concrete = it.filter { !it.isInterface }.map { it.name } - return interfaces.plus(concrete) + log.trace { "Derived contract types: ${interfaces.union(concrete)}" } + return interfaces.union(concrete) } } @@ -236,17 +234,10 @@ class HibernateQueryCriteriaParser(val contractType: Class, val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultFungibleStates.get("stateRef")) predicateSet.add(joinPredicate) - // contract State Types - val contractTypes = deriveContractTypes() - if (contractTypes.isNotEmpty()) - predicateSet.add(criteriaBuilder.and(vaultStates.get("contractStateClassName").`in`(contractTypes))) - // owner criteria.owner?.let { - val ownerKeys = criteria.owner as List - val joinFungibleStateToParty = vaultFungibleStates.join("issuerParty") - val owners = ownerKeys.map { it.nameOrNull()?.toString() ?: it.toString()} - predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(owners))) + val owners = criteria.owner as List + predicateSet.add(criteriaBuilder.and(vaultFungibleStates.get("owner").`in`(owners))) } // quantity @@ -255,11 +246,9 @@ class HibernateQueryCriteriaParser(val contractType: Class, } // issuer party - criteria.issuerPartyName?.let { - val issuerParties = criteria.issuerPartyName as List - val joinFungibleStateToParty = vaultFungibleStates.join("issuerParty") - val issuerPartyNames = issuerParties.map { it.nameOrNull().toString() } - predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(issuerPartyNames))) + criteria.issuer?.let { + val issuerParties = criteria.issuer as List + predicateSet.add(criteriaBuilder.and(vaultFungibleStates.get("issuer").`in`(issuerParties))) } // issuer reference @@ -271,9 +260,8 @@ class HibernateQueryCriteriaParser(val contractType: Class, // participants criteria.participants?.let { val participants = criteria.participants as List - val joinFungibleStateToParty = vaultFungibleStates.join("participants") - val participantKeys = participants.map { it.nameOrNull().toString() } - predicateSet.add(criteriaBuilder.and(joinFungibleStateToParty.get("name").`in`(participantKeys))) + val joinLinearStateToParty = vaultFungibleStates.joinSet("participants") + predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) criteriaQuery.distinct(true) } return predicateSet @@ -290,11 +278,6 @@ class HibernateQueryCriteriaParser(val contractType: Class, val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) joinPredicates.add(joinPredicate) - // contract State Types - val contractTypes = deriveContractTypes() - if (contractTypes.isNotEmpty()) - predicateSet.add(criteriaBuilder.and(vaultStates.get("contractStateClassName").`in`(contractTypes))) - // linear ids UUID criteria.uuid?.let { val uuids = criteria.uuid as List @@ -304,15 +287,15 @@ class HibernateQueryCriteriaParser(val contractType: Class, // linear ids externalId criteria.externalId?.let { val externalIds = criteria.externalId as List - predicateSet.add(criteriaBuilder.and(vaultLinearStates.get("externalId").`in`(externalIds))) + if (externalIds.isNotEmpty()) + predicateSet.add(criteriaBuilder.and(vaultLinearStates.get("externalId").`in`(externalIds))) } // deal participants criteria.participants?.let { val participants = criteria.participants as List - val joinLinearStateToParty = vaultLinearStates.join("participants") - val participantKeys = participants.map { it.nameOrNull().toString() } - predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.get("name").`in`(participantKeys))) + val joinLinearStateToParty = vaultLinearStates.joinSet("participants") + predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) criteriaQuery.distinct(true) } return predicateSet @@ -331,11 +314,6 @@ class HibernateQueryCriteriaParser(val contractType: Class, val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) joinPredicates.add(joinPredicate) - // contract State Types - val contractTypes = deriveContractTypes() - if (contractTypes.isNotEmpty()) - predicateSet.add(criteriaBuilder.and(vaultStates.get("contractStateClassName").`in`(contractTypes))) - // resolve general criteria expressions parseExpression(entityRoot, criteria.expression, predicateSet) } @@ -388,11 +366,11 @@ class HibernateQueryCriteriaParser(val contractType: Class, val selections = if (aggregateExpressions.isEmpty()) - listOf(vaultStates).plus(rootEntities.map { it.value }) + rootEntities.map { it.value } else aggregateExpressions criteriaQuery.multiselect(selections) - val combinedPredicates = joinPredicates.plus(predicateSet) + val combinedPredicates = joinPredicates.plus(predicateSet).plus(commonPredicates.values) criteriaQuery.where(*combinedPredicates.toTypedArray()) return predicateSet @@ -400,14 +378,39 @@ class HibernateQueryCriteriaParser(val contractType: Class, override fun parseCriteria(criteria: CommonQueryCriteria): Collection { log.trace { "Parsing CommonQueryCriteria: $criteria" } - val predicateSet = mutableSetOf() // state status stateTypes = criteria.status - if (criteria.status != Vault.StateStatus.ALL) - predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) + if (criteria.status != Vault.StateStatus.ALL) { + val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EqualityComparisonOperator.EQUAL) + if (commonPredicates.containsKey(predicateID)) { + val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal + if (existingStatus != criteria.status) { + log.warn("Overriding previous attribute [${VaultSchemaV1.VaultStates::stateStatus.name}] value $existingStatus with ${criteria.status}") + commonPredicates.replace(predicateID, criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status)) + } + } + else { + commonPredicates.put(predicateID, criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), criteria.status)) + } + } - return predicateSet + // contract state types + val contractTypes = deriveContractTypes(criteria.contractStateTypes) + if (contractTypes.isNotEmpty()) { + val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, CollectionOperator.IN) + if (commonPredicates.containsKey(predicateID)) { + val existingTypes = (commonPredicates[predicateID]!!.expressions[0] as InPredicate<*>).values.map { (it as LiteralExpression).literal }.toSet() + if (existingTypes != contractTypes) { + log.warn("Enriching previous attribute [${VaultSchemaV1.VaultStates::contractStateClassName.name}] values [$existingTypes] with [$contractTypes]") + commonPredicates.replace(predicateID, criteriaBuilder.and(vaultStates.get(VaultSchemaV1.VaultStates::contractStateClassName.name).`in`(contractTypes.plus(existingTypes)))) + } + } else { + commonPredicates.put(predicateID, criteriaBuilder.and(vaultStates.get(VaultSchemaV1.VaultStates::contractStateClassName.name).`in`(contractTypes))) + } + } + + return emptySet() } private fun parse(sorting: Sort) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt index 3bccc8597c..b6cfebe4cd 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateVaultQueryImpl.kt @@ -1,16 +1,17 @@ package net.corda.node.services.vault import net.corda.core.internal.ThreadBox -import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.internal.bufferUntilSubscribed import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.VaultQueryService +import net.corda.core.node.services.VaultService import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT @@ -18,18 +19,17 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import net.corda.node.services.database.HibernateConfiguration -import org.jetbrains.exposed.sql.transactions.TransactionManager -import rx.subjects.PublishSubject +import net.corda.node.utilities.DatabaseTransactionManager +import org.hibernate.Session import rx.Observable import java.lang.Exception import java.util.* -import javax.persistence.EntityManager import javax.persistence.Tuple - class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, - val updatesPublisher: PublishSubject>) : SingletonSerializeAsToken(), VaultQueryService { + val vault: VaultService) : SingletonSerializeAsToken(), VaultQueryService { companion object { val log = loggerFor() } @@ -37,6 +37,29 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, private val sessionFactory = hibernateConfig.sessionFactoryForRegisteredSchemas() private val criteriaBuilder = sessionFactory.criteriaBuilder + /** + * Maintain a list of contract state interfaces to concrete types stored in the vault + * for usage in generic queries of type queryBy or queryBy> + */ + private val contractTypeMappings = bootstrapContractStateTypes() + + init { + vault.rawUpdates.subscribe { update -> + update.produced.forEach { + val concreteType = it.state.data.javaClass + log.trace { "State update of type: $concreteType" } + val seen = contractTypeMappings.any { it.value.contains(concreteType.name) } + if (!seen) { + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractTypeMappings.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } + } + } + } + } + @Throws(VaultQueryException::class) override fun _queryBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): Vault.Page { log.info("Vault Query for contract type: $contractType, criteria: $criteria, pagination: $paging, sorting: $sorting") @@ -50,15 +73,12 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, totalStates = results.otherResults[0] as Long } - val session = sessionFactory.withOptions(). - connection(TransactionManager.current().connection). - openSession() + val session = getSession() session.use { val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) - val contractTypeMappings = resolveUniqueContractStateTypes(session) // TODO: revisit (use single instance of parser for all queries) val criteriaParser = HibernateQueryCriteriaParser(contractType, contractTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) @@ -98,7 +118,14 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, val vaultState = result[0] as VaultSchemaV1.VaultStates val stateRef = StateRef(SecureHash.parse(vaultState.stateRef!!.txId!!), vaultState.stateRef!!.index!!) val state = vaultState.contractState.deserialize>(context = STORAGE_CONTEXT) - statesMeta.add(Vault.StateMetadata(stateRef, vaultState.contractStateClassName, vaultState.recordedTime, vaultState.consumedTime, vaultState.stateStatus, vaultState.notaryName, vaultState.notaryKey, vaultState.lockId, vaultState.lockUpdateTime)) + statesMeta.add(Vault.StateMetadata(stateRef, + vaultState.contractStateClassName, + vaultState.recordedTime, + vaultState.consumedTime, + vaultState.stateStatus, + vaultState.notary, + vaultState.lockId, + vaultState.lockUpdateTime)) statesAndRefs.add(StateAndRef(state, stateRef)) } else { @@ -116,41 +143,49 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration, } } - private val mutex = ThreadBox({ updatesPublisher }) + private val mutex = ThreadBox({ vault.updatesPublisher }) @Throws(VaultQueryException::class) override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractType: Class): DataFeed, Vault.Update> { return mutex.locked { val snapshotResults = _queryBy(criteria, paging, sorting, contractType) @Suppress("UNCHECKED_CAST") - val updates = updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractType, snapshotResults.stateTypes) } as Observable> + val updates = vault.updatesPublisher.bufferUntilSubscribed().filter { it.containsType(contractType, snapshotResults.stateTypes) } as Observable> DataFeed(snapshotResults, updates) } } + private fun getSession(): Session { + return sessionFactory.withOptions(). + connection(DatabaseTransactionManager.current().connection). + openSession() + } + /** - * Maintain a list of contract state interfaces to concrete types stored in the vault - * for usage in generic queries of type queryBy or queryBy> + * Derive list from existing vault states and then incrementally update using vault observables */ - fun resolveUniqueContractStateTypes(session: EntityManager): Map> { + fun bootstrapContractStateTypes(): MutableMap> { val criteria = criteriaBuilder.createQuery(String::class.java) val vaultStates = criteria.from(VaultSchemaV1.VaultStates::class.java) criteria.select(vaultStates.get("contractStateClassName")).distinct(true) - val query = session.createQuery(criteria) - val results = query.resultList - val distinctTypes = results.map { it } + val session = getSession() + session.use { + val query = session.createQuery(criteria) + val results = query.resultList + val distinctTypes = results.map { it } - val contractInterfaceToConcreteTypes = mutableMapOf>() - distinctTypes.forEach { it -> - @Suppress("UNCHECKED_CAST") - val concreteType = Class.forName(it) as Class - val contractInterfaces = deriveContractInterfaces(concreteType) - contractInterfaces.map { - val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableListOf() }) - contractInterface.add(concreteType.name) + val contractInterfaceToConcreteTypes = mutableMapOf>() + distinctTypes.forEach { type -> + @Suppress("UNCHECKED_CAST") + val concreteType = Class.forName(type) as Class + val contractInterfaces = deriveContractInterfaces(concreteType) + contractInterfaces.map { + val contractInterface = contractInterfaceToConcreteTypes.getOrPut(it.name, { mutableSetOf() }) + contractInterface.add(concreteType.name) + } } + return contractInterfaceToConcreteTypes } - return contractInterfaceToConcreteTypes } private fun deriveContractInterfaces(clazz: Class): Set> { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 04e98b5e75..64fb9c81fa 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -2,15 +2,11 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand -import com.google.common.annotations.VisibleForTesting -import io.requery.PersistenceException -import io.requery.kotlin.eq -import io.requery.query.RowExpression import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny -import net.corda.core.crypto.toBase58String import net.corda.core.internal.ThreadBox +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.tee import net.corda.core.node.ServiceHub import net.corda.core.node.services.StatesNotAvailableException @@ -21,6 +17,7 @@ import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.SortAttribute import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize @@ -28,22 +25,15 @@ import net.corda.core.serialization.serialize import net.corda.core.transactions.CoreTransaction import net.corda.core.transactions.NotaryChangeWireTransaction import net.corda.core.transactions.WireTransaction -import net.corda.core.utilities.NonEmptySet -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.toNonEmptySet -import net.corda.core.utilities.trace -import net.corda.node.services.database.RequeryConfiguration -import net.corda.node.services.database.parserTransactionIsolationLevel +import net.corda.core.utilities.* import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.node.services.vault.schemas.requery.Models -import net.corda.node.services.vault.schemas.requery.VaultSchema -import net.corda.node.services.vault.schemas.requery.VaultStatesEntity -import net.corda.node.services.vault.schemas.requery.VaultTxnNoteEntity +import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.wrapWithDatabaseTransaction import rx.Observable import rx.subjects.PublishSubject import java.security.PublicKey +import java.time.Instant import java.util.* import javax.persistence.criteria.Predicate @@ -54,23 +44,15 @@ import javax.persistence.criteria.Predicate * This class needs database transactions to be in-flight during method calls and init, and will throw exceptions if * this is not the case. * - * TODO: move query / filter criteria into the database query. * TODO: keep an audit trail with time stamps of previously unconsumed states "as of" a particular point in time. * TODO: have transaction storage do some caching. */ -class NodeVaultService(private val services: ServiceHub, dataSourceProperties: Properties, databaseProperties: Properties?) : SingletonSerializeAsToken(), VaultService { +class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsToken(), VaultService { private companion object { val log = loggerFor() - - // Define composite primary key used in Requery Expression - val stateRefCompositeColumn: RowExpression = RowExpression.of(listOf(VaultStatesEntity.TX_ID, VaultStatesEntity.INDEX)) } - val configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = databaseProperties ?: Properties()) - val session = configuration.sessionForModel(Models.VAULT) - private val transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties?.getProperty("transactionIsolationLevel") ?:"") - private class InnerState { val _updatesPublisher = PublishSubject.create>()!! val _rawUpdatesPublisher = PublishSubject.create>()!! @@ -89,36 +71,29 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P val consumedStateRefs = update.consumed.map { it.ref } log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." } - session.withTransaction(transactionIsolationLevel) { - producedStateRefsMap.forEach { it -> - val state = VaultStatesEntity().apply { - txId = it.key.txhash.toString() - index = it.key.index - stateStatus = Vault.StateStatus.UNCONSUMED - contractStateClassName = it.value.state.data.javaClass.name - contractState = it.value.state.serialize(context = STORAGE_CONTEXT).bytes - notaryName = it.value.state.notary.name.toString() - notaryKey = it.value.state.notary.owningKey.toBase58String() - recordedTime = services.clock.instant() - } - insert(state) - } - // TODO: awaiting support of UPDATE WHERE IN in Requery DSL - consumedStateRefs.forEach { stateRef -> - val queryKey = io.requery.proxy.CompositeKey(mapOf(VaultStatesEntity.TX_ID to stateRef.txhash.toString(), - VaultStatesEntity.INDEX to stateRef.index)) - val state = findByKey(VaultStatesEntity::class, queryKey) - state?.run { - stateStatus = Vault.StateStatus.CONSUMED - consumedTime = services.clock.instant() - // remove lock (if held) - if (lockId != null) { - lockId = null - lockUpdateTime = services.clock.instant() - log.trace("Releasing soft lock on consumed state: $stateRef") - } - update(state) + val session = DatabaseTransactionManager.current().session + producedStateRefsMap.forEach { stateAndRef -> + val state = VaultSchemaV1.VaultStates( + notary = stateAndRef.value.state.notary, + contractStateClassName = stateAndRef.value.state.data.javaClass.name, + contractState = stateAndRef.value.state.serialize(context = STORAGE_CONTEXT).bytes, + stateStatus = Vault.StateStatus.UNCONSUMED, + recordedTime = services.clock.instant()) + state.stateRef = PersistentStateRef(stateAndRef.key) + session.save(state) + } + consumedStateRefs.forEach { stateRef -> + val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) + state?.run { + stateStatus = Vault.StateStatus.CONSUMED + consumedTime = services.clock.instant() + // remove lock (if held) + if (lockId != null) { + lockId = null + lockUpdateTime = services.clock.instant() + log.trace("Releasing soft lock on consumed state: $stateRef") } + session.save(state) } } } @@ -221,19 +196,25 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P processAndNotify(netDelta) } + // TODO: replace this method in favour of a VaultQuery query private fun loadStates(refs: Collection): HashSet> { val states = HashSet>() if (refs.isNotEmpty()) { - session.withTransaction(transactionIsolationLevel) { - val result = select(VaultStatesEntity::class). - where(stateRefCompositeColumn.`in`(stateRefArgs(refs))). - and(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED) - result.get().forEach { - val txHash = SecureHash.parse(it.txId) - val index = it.index - val state = it.contractState.deserialize>(context = STORAGE_CONTEXT) - states.add(StateAndRef(state, StateRef(txHash, index))) - } + val session = DatabaseTransactionManager.current().session + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val statusPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val persistentStateRefs = refs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + criteriaQuery.where(statusPredicate, stateRefsPredicate) + val results = session.createQuery(criteriaQuery).resultList + results.asSequence().forEach { + val txHash = SecureHash.parse(it.stateRef?.txId!!) + val index = it.stateRef?.index!! + val state = it.contractState.deserialize>(context = STORAGE_CONTEXT) + states.add(StateAndRef(state, StateRef(txHash, index))) } } return states @@ -252,161 +233,109 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P } override fun addNoteToTransaction(txnId: SecureHash, noteText: String) { - session.withTransaction(transactionIsolationLevel) { - val txnNoteEntity = VaultTxnNoteEntity() - txnNoteEntity.txId = txnId.toString() - txnNoteEntity.note = noteText - insert(txnNoteEntity) - } + val txnNoteEntity = VaultSchemaV1.VaultTxnNote(txnId.toString(), noteText) + DatabaseTransactionManager.current().session.save(txnNoteEntity) } override fun getTransactionNotes(txnId: SecureHash): Iterable { - return session.withTransaction(transactionIsolationLevel) { - (select(VaultSchema.VaultTxnNote::class) where (VaultSchema.VaultTxnNote::txId eq txnId.toString())).get().asIterable().map { it.note } - } + val session = DatabaseTransactionManager.current().session + val criteriaBuilder = session.criteriaBuilder + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultTxnNote::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultTxnNote::class.java) + val txIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultTxnNote::txId.name), txnId.toString()) + criteriaQuery.where(txIdPredicate) + val results = session.createQuery(criteriaQuery).resultList + return results.asIterable().map { it.note } } @Throws(StatesNotAvailableException::class) override fun softLockReserve(lockId: UUID, stateRefs: NonEmptySet) { val softLockTimestamp = services.clock.instant() - val stateRefArgs = stateRefArgs(stateRefs) try { - session.withTransaction(transactionIsolationLevel) { - val updatedRows = update(VaultStatesEntity::class) - .set(VaultStatesEntity.LOCK_ID, lockId.toString()) - .set(VaultStatesEntity.LOCK_UPDATE_TIME, softLockTimestamp) - .where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED) - .and((VaultStatesEntity.LOCK_ID eq lockId.toString()) or (VaultStatesEntity.LOCK_ID.isNull())) - .and(stateRefCompositeColumn.`in`(stateRefArgs)).get().value() - if (updatedRows > 0 && updatedRows == stateRefs.size) { - log.trace("Reserving soft lock states for $lockId: $stateRefs") - FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true - } else { - // revert partial soft locks - val revertUpdatedRows = update(VaultStatesEntity::class) - .set(VaultStatesEntity.LOCK_ID, null) - .where(VaultStatesEntity.LOCK_UPDATE_TIME eq softLockTimestamp) - .and(VaultStatesEntity.LOCK_ID eq lockId.toString()) - .and(stateRefCompositeColumn.`in`(stateRefArgs)).get().value() - if (revertUpdatedRows > 0) { - log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId") - } - throw StatesNotAvailableException("Attempted to reserve $stateRefs for $lockId but only $updatedRows rows available") + val session = DatabaseTransactionManager.current().session + val criteriaBuilder = session.criteriaBuilder + val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) + val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.or(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name).isNull, + criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString())) + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) + val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + if (updatedRows > 0 && updatedRows == stateRefs.size) { + log.trace("Reserving soft lock states for $lockId: $stateRefs") + FlowStateMachineImpl.currentStateMachine()?.hasSoftLockedStates = true + } else { + // revert partial soft locks + val criteriaRevertUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) + val vaultStatesRevert = criteriaRevertUpdate.from(VaultSchemaV1.VaultStates::class.java) + val lockIdPredicateRevert = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + val lockUpdateTime = criteriaBuilder.equal(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + val persistentStateRefsRevert = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKeyRevert = vaultStatesRevert.get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicateRevert = criteriaBuilder.and(compositeKeyRevert.`in`(persistentStateRefsRevert)) + criteriaRevertUpdate.set(vaultStatesRevert.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + criteriaRevertUpdate.where(lockUpdateTime, lockIdPredicateRevert, stateRefsPredicateRevert) + val revertUpdatedRows = session.createQuery(criteriaRevertUpdate).executeUpdate() + if (revertUpdatedRows > 0) { + log.trace("Reverting $revertUpdatedRows partially soft locked states for $lockId") } + throw StatesNotAvailableException("Attempted to reserve $stateRefs for $lockId but only $updatedRows rows available") } - } catch (e: PersistenceException) { + } catch (e: Exception) { log.error("""soft lock update error attempting to reserve states for $lockId and $stateRefs") $e. """) if (e.cause is StatesNotAvailableException) throw (e.cause as StatesNotAvailableException) + throw e } } override fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet?) { + val softLockTimestamp = services.clock.instant() + val session = DatabaseTransactionManager.current().session + val criteriaBuilder = session.criteriaBuilder if (stateRefs == null) { - session.withTransaction(transactionIsolationLevel) { - val update = update(VaultStatesEntity::class) - .set(VaultStatesEntity.LOCK_ID, null) - .set(VaultStatesEntity.LOCK_UPDATE_TIME, services.clock.instant()) - .where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED) - .and(VaultStatesEntity.LOCK_ID eq lockId.toString()).get() - if (update.value() > 0) { - log.trace("Releasing ${update.value()} soft locked states for $lockId") - } + val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) + val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + criteriaUpdate.where(stateStatusPredication, lockIdPredicate) + val update = session.createQuery(criteriaUpdate).executeUpdate() + if (update > 0) { + log.trace("Releasing $update soft locked states for $lockId") } } else { try { - session.withTransaction(transactionIsolationLevel) { - val updatedRows = update(VaultStatesEntity::class) - .set(VaultStatesEntity.LOCK_ID, null) - .set(VaultStatesEntity.LOCK_UPDATE_TIME, services.clock.instant()) - .where(VaultStatesEntity.STATE_STATUS eq Vault.StateStatus.UNCONSUMED) - .and(VaultStatesEntity.LOCK_ID eq lockId.toString()) - .and(stateRefCompositeColumn.`in`(stateRefArgs(stateRefs))).get().value() - if (updatedRows > 0) { - log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs") - } + val criteriaUpdate = criteriaBuilder.createCriteriaUpdate(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaUpdate.from(VaultSchemaV1.VaultStates::class.java) + val stateStatusPredication = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::stateStatus.name), Vault.StateStatus.UNCONSUMED) + val lockIdPredicate = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), lockId.toString()) + val persistentStateRefs = stateRefs.map { PersistentStateRef(it.txhash.bytes.toHexString(), it.index) } + val compositeKey = vaultStates.get(VaultSchemaV1.VaultStates::stateRef.name) + val stateRefsPredicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockId.name), criteriaBuilder.nullLiteral(String::class.java)) + criteriaUpdate.set(vaultStates.get(VaultSchemaV1.VaultStates::lockUpdateTime.name), softLockTimestamp) + criteriaUpdate.where(stateStatusPredication, lockIdPredicate, stateRefsPredicate) + val updatedRows = session.createQuery(criteriaUpdate).executeUpdate() + if (updatedRows > 0) { + log.trace("Releasing $updatedRows soft locked states for $lockId and stateRefs $stateRefs") } - } catch (e: PersistenceException) { + } catch (e: Exception) { log.error("""soft lock update error attempting to release states for $lockId and $stateRefs") $e. """) + throw e } } } - // TODO We shouldn't need to rewrite the query if we could modify the defaults. - private class QueryEditor(val services: ServiceHub, - val lockId: UUID, - val contractType: Class) : IQueryCriteriaParser { - var alreadyHasVaultQuery: Boolean = false - var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType), - softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)), - status = Vault.StateStatus.UNCONSUMED) - - override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection { - modifiedCriteria = criteria - return emptyList() - } - - override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection { - modifiedCriteria = criteria - return emptyList() - } - - override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection { - modifiedCriteria = criteria - return emptyList() - } - - override fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection { - modifiedCriteria = criteria - return emptyList() - } - - override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection { - modifiedCriteria = criteria.copy(contractStateTypes = setOf(contractType), - softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)), - status = Vault.StateStatus.UNCONSUMED) - alreadyHasVaultQuery = true - return emptyList() - } - - override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection { - parse(left) - val modifiedLeft = modifiedCriteria - parse(right) - val modifiedRight = modifiedCriteria - modifiedCriteria = modifiedLeft.or(modifiedRight) - return emptyList() - } - - override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection { - parse(left) - val modifiedLeft = modifiedCriteria - parse(right) - val modifiedRight = modifiedCriteria - modifiedCriteria = modifiedLeft.and(modifiedRight) - return emptyList() - } - - override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection { - val basicQuery = modifiedCriteria - criteria.visit(this) - modifiedCriteria = if (alreadyHasVaultQuery) modifiedCriteria else criteria.and(basicQuery) - return emptyList() - } - - fun queryForEligibleStates(criteria: QueryCriteria): Vault.Page { - val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) - val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) - parse(criteria, sorter) - - return services.vaultQueryService.queryBy(contractType, modifiedCriteria, sorter) - } - } - - @Suspendable @Throws(StatesNotAvailableException::class) override fun , U : Any> tryLockFungibleStatesForSpending(lockId: UUID, @@ -417,9 +346,13 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P return emptyList() } - // TODO This helper code re-writes the query to alter the defaults on things such as soft locks - // and then runs the query. Ideally we would not need to do this. - val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery) + // Enrich QueryCriteria with additional default attributes (such as soft locks) + val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) + val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) + val enrichedCriteria = QueryCriteria.VaultQueryCriteria( + contractStateTypes = setOf(contractType), + softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId))) + val results = services.vaultQueryService.queryBy(contractType, enrichedCriteria.and(eligibleStatesQuery), sorter) var claimedAmount = 0L val claimedStates = mutableListOf>() @@ -463,11 +396,4 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P is LinearState -> state.isRelevant(ourKeys) else -> ourKeys.intersect(state.participants.map { it.owningKey }).isNotEmpty() } - - /** - * Helper method to generate a string formatted list of Composite Keys for Requery Expression clause - */ - private fun stateRefArgs(stateRefs: Iterable): List> { - return stateRefs.map { listOf("'${it.txhash}'", it.index) } - } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 9ead62d2c9..405205ae53 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -1,13 +1,16 @@ package net.corda.node.services.vault +import net.corda.core.contracts.ContractState import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault -import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.OpaqueBytes +import org.hibernate.annotations.Generated +import org.hibernate.annotations.GenerationTime +import java.io.Serializable import java.time.Instant import java.util.* import javax.persistence.* @@ -22,17 +25,14 @@ object VaultSchema */ @CordaSerializable object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, version = 1, - mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, CommonSchemaV1.Party::class.java)) { + mappedTypes = listOf(VaultStates::class.java, VaultLinearStates::class.java, VaultFungibleStates::class.java, VaultTxnNote::class.java)) { @Entity @Table(name = "vault_states", indexes = arrayOf(Index(name = "state_status_idx", columnList = "state_status"))) class VaultStates( - /** refers to the notary a state is attached to */ + /** refers to the X500Name of the notary a state is attached to */ @Column(name = "notary_name") - var notaryName: String, - - @Column(name = "notary_key", length = 65535) // TODO What is the upper limit on size of CompositeKey? - var notaryKey: String, + var notary: AbstractParty, /** references a concrete ContractState that is [QueryableState] and has a [MappedSchema] */ @Column(name = "contract_state_class_name") @@ -53,16 +53,16 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio /** refers to timestamp recorded upon entering CONSUMED state */ @Column(name = "consumed_timestamp", nullable = true) - var consumedTime: Instant?, + var consumedTime: Instant? = null, /** used to denote a state has been soft locked (to prevent double spend) * will contain a temporary unique [UUID] obtained from a flow session */ @Column(name = "lock_id", nullable = true) - var lockId: String, + var lockId: String? = null, /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ @Column(name = "lock_timestamp", nullable = true) - var lockUpdateTime: Instant? + var lockUpdateTime: Instant? = null ) : PersistentState() @Entity @@ -71,8 +71,13 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio Index(name = "uuid_index", columnList = "uuid"))) class VaultLinearStates( /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, + // Reason for not using Set is described here: + // https://stackoverflow.com/questions/44213074/kotlin-collection-has-neither-generic-type-or-onetomany-targetentity /** * Represents a [LinearState] [UniqueIdentifier] @@ -86,18 +91,23 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio constructor(uid: UniqueIdentifier, _participants: List) : this(externalId = uid.externalId, uuid = uid.id, - participants = _participants.map{ CommonSchemaV1.Party(it) }.toSet() ) + participants = _participants.toMutableSet()) } @Entity @Table(name = "vault_fungible_states") class VaultFungibleStates( /** [ContractState] attributes */ - @OneToMany(cascade = arrayOf(CascadeType.ALL)) - var participants: Set, + + /** X500Name of participant parties **/ + @ElementCollection + @Column(name = "participants") + var participants: MutableSet? = null, /** [OwnableState] attributes */ - @Column(name = "owner_id") + + /** X500Name of owner party **/ + @Column(name = "owner_name") var owner: AbstractParty, /** [FungibleAsset] attributes @@ -111,8 +121,10 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio var quantity: Long, /** Issuer attributes */ - @OneToOne(cascade = arrayOf(CascadeType.ALL)) - var issuerParty: CommonSchemaV1.Party, + + /** X500Name of issuer party **/ + @Column(name = "issuer_name") + var issuer: AbstractParty, @Column(name = "issuer_reference") var issuerRef: ByteArray @@ -120,8 +132,27 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : this(owner = _owner, quantity = _quantity, - issuerParty = CommonSchemaV1.Party(_issuerParty), + issuer = _issuerParty, issuerRef = _issuerRef.bytes, - participants = _participants.map { CommonSchemaV1.Party(it) }.toSet()) + participants = _participants.toMutableSet()) + } + + @Entity + @Table(name = "vault_transaction_notes", + indexes = arrayOf(Index(name = "seq_no_index", columnList = "seq_no"), + Index(name = "transaction_id_index", columnList = "transaction_id"))) + class VaultTxnNote( + @Id + @GeneratedValue + @Column(name = "seq_no") + var seqNo: Int, + + @Column(name = "transaction_id", length = 64) + var txId: String, + + @Column(name = "note") + var note: String + ) : Serializable { + constructor(txId: String, note: String) : this(0, txId, note) } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt index c9fb26b8b3..e18f7d1d16 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/AppendOnlyPersistentMap.kt @@ -5,11 +5,11 @@ import java.util.* /** - * Implements a caching layer on top of an *append-only* table accessed via Hibernate mapping. Note that if the same key is [put] twice the + * Implements a caching layer on top of an *append-only* table accessed via Hibernate mapping. Note that if the same key is [set] twice the * behaviour is unpredictable! There is a best-effort check for double inserts, but this should *not* be relied on, so * ONLY USE THIS IF YOUR TABLE IS APPEND-ONLY */ -class AppendOnlyPersistentMap ( +class AppendOnlyPersistentMap ( val toPersistentEntityKey: (K) -> EK, val fromPersistentEntity: (E) -> Pair, val toPersistentEntity: (key: K, value: V) -> E, @@ -34,6 +34,8 @@ class AppendOnlyPersistentMap ( return cache.get(key).orElse(null) } + val size get() = allPersisted().toList().size + /** * Returns all key/value pairs from the underlying storage. */ @@ -79,35 +81,54 @@ class AppendOnlyPersistentMap ( } /** - * Puts the value into the map and the underlying storage. - * Inserting the duplicated key may be unpredictable. + * Associates the specified value with the specified key in this map and persists it. + * If the map previously contained a mapping for the key, the behaviour is unpredictable and may throw an error from the underlying storage. */ operator fun set(key: K, value: V) = set(key, value, logWarning = false) { - key,value -> DatabaseTransactionManager.current().session.save(toPersistentEntity(key,value)) + k, v -> DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) null } /** - * Puts the value into the map and underlying storage. - * Duplicated key is not added into the map and underlying storage. + * Associates the specified value with the specified key in this map and persists it. + * If the map previously contained a mapping for the key, the old value is not replaced. * @return true if added key was unique, otherwise false */ fun addWithDuplicatesAllowed(key: K, value: V): Boolean = set(key, value) { - key, value -> - val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + k, v -> + val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) if (existingEntry == null) { - DatabaseTransactionManager.current().session.save(toPersistentEntity(key,value)) + DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) null } else { fromPersistentEntity(existingEntry).second } } + fun putAll(entries: Map) { + entries.forEach { + set(it.key, it.value) + } + } + private fun loadValue(key: K): V? { val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) return result?.let(fromPersistentEntity)?.second } + operator fun contains(key: K) = get(key) != null + + /** + * Removes all of the mappings from this map and underlying storage. The map will be empty after this call returns. + * WARNING!! The method is not thread safe. + */ + fun clear() { + val session = DatabaseTransactionManager.current().session + val deleteQuery = session.criteriaBuilder.createCriteriaDelete(persistentEntityClass) + deleteQuery.from(persistentEntityClass) + session.createQuery(deleteQuery).executeUpdate() + cache.invalidateAll() + } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt index a29fee242e..b8add3a202 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/CordaPersistence.kt @@ -3,11 +3,10 @@ package net.corda.node.utilities import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariDataSource import net.corda.core.node.services.IdentityService -import net.corda.core.schemas.MappedSchema +import net.corda.node.services.api.SchemaService import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.NodeSchemaService import org.hibernate.SessionFactory -import org.jetbrains.exposed.sql.Database import rx.Observable import rx.Subscriber @@ -18,23 +17,31 @@ import java.sql.SQLException import java.util.* import java.util.concurrent.CopyOnWriteArrayList +/** + * Table prefix for all tables owned by the node module. + */ +const val NODE_DATABASE_PREFIX = "node_" //HikariDataSource implements Closeable which allows CordaPersistence to be Closeable -class CordaPersistence(var dataSource: HikariDataSource, var nodeSchemaService: NodeSchemaService, val identitySvc: ()-> IdentityService, databaseProperties: Properties): Closeable { - - /** Holds Exposed database, the field will be removed once Exposed library is removed */ - lateinit var database: Database +class CordaPersistence(var dataSource: HikariDataSource, private var createSchemaService: () -> SchemaService, + private val createIdentityService: ()-> IdentityService, databaseProperties: Properties): Closeable { var transactionIsolationLevel = parserTransactionIsolationLevel(databaseProperties.getProperty("transactionIsolationLevel")) - val entityManagerFactory: SessionFactory by lazy(LazyThreadSafetyMode.NONE) { + val hibernateConfig: HibernateConfiguration by lazy { transaction { - HibernateConfiguration(nodeSchemaService, databaseProperties, identitySvc).sessionFactoryForRegisteredSchemas() + HibernateConfiguration(createSchemaService, databaseProperties, createIdentityService) + } + } + + val entityManagerFactory: SessionFactory by lazy { + transaction { + hibernateConfig.sessionFactoryForRegisteredSchemas() } } companion object { - fun connect(dataSource: HikariDataSource, nodeSchemaService: NodeSchemaService, identitySvc: () -> IdentityService, databaseProperties: Properties): CordaPersistence { - return CordaPersistence(dataSource, nodeSchemaService, identitySvc, databaseProperties).apply { + fun connect(dataSource: HikariDataSource, createSchemaService: () -> SchemaService, createIdentityService: () -> IdentityService, databaseProperties: Properties): CordaPersistence { + return CordaPersistence(dataSource, createSchemaService, createIdentityService, databaseProperties).apply { DatabaseTransactionManager(this) } } @@ -100,14 +107,10 @@ class CordaPersistence(var dataSource: HikariDataSource, var nodeSchemaService: } } -fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, entitySchemas: Set = emptySet(), identitySvc: ()-> IdentityService): CordaPersistence { +fun configureDatabase(dataSourceProperties: Properties, databaseProperties: Properties?, createSchemaService: () -> SchemaService = { NodeSchemaService() }, createIdentityService: () -> IdentityService): CordaPersistence { val config = HikariConfig(dataSourceProperties) val dataSource = HikariDataSource(config) - val persistence = CordaPersistence.connect(dataSource, NodeSchemaService(entitySchemas), identitySvc, databaseProperties ?: Properties()) - - //org.jetbrains.exposed.sql.Database will be removed once Exposed library is removed - val database = Database.connect(dataSource) { _ -> ExposedTransactionManager() } - persistence.database = database + val persistence = CordaPersistence.connect(dataSource, createSchemaService, createIdentityService, databaseProperties ?: Properties()) // Check not in read-only mode. persistence.transaction { @@ -197,7 +200,7 @@ fun rx.Observable.wrapWithDatabaseTransaction(db: CordaPersistence? wrappingSubscriber.cleanUp() // If cleanup removed the last subscriber reset the system, as future subscribers might need the stream again if (wrappingSubscriber.delegates.isEmpty()) { - wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) + wrappingSubscriber = DatabaseTransactionWrappingSubscriber(db) } } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt deleted file mode 100644 index 028117015c..0000000000 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt +++ /dev/null @@ -1,209 +0,0 @@ -package net.corda.node.utilities - -import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.parsePublicKeyBase58 -import net.corda.core.crypto.toBase58String -import org.bouncycastle.cert.X509CertificateHolder -import org.h2.jdbc.JdbcBlob -import org.jetbrains.exposed.sql.* -import java.io.ByteArrayInputStream -import java.security.PublicKey -import java.security.cert.CertPath -import java.security.cert.CertificateFactory -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.ZoneOffset -import java.util.* - -/** - * Table prefix for all tables owned by the node module. - */ -const val NODE_DATABASE_PREFIX = "node_" - -// Composite columns for use with below Exposed helpers. -data class PartyColumns(val name: Column, val owningKey: Column) -data class PartyAndCertificateColumns(val name: Column, val owningKey: Column, - val certificate: Column, val certPath: Column) -data class StateRefColumns(val txId: Column, val index: Column) -data class TxnNoteColumns(val txId: Column, val note: Column) - -/** - * [Table] column helpers for use with Exposed, as per [varchar] etc. - */ -fun Table.certificate(name: String) = this.registerColumn(name, X509CertificateColumnType) -fun Table.certificatePath(name: String) = this.registerColumn(name, CertPathColumnType) -fun Table.publicKey(name: String) = this.registerColumn(name, PublicKeyColumnType) -fun Table.secureHash(name: String) = this.registerColumn(name, SecureHashColumnType) -fun Table.party(nameColumnName: String, - keyColumnName: String) = PartyColumns(this.varchar(nameColumnName, length = 255), this.publicKey(keyColumnName)) -fun Table.partyAndCertificate(nameColumnName: String, - keyColumnName: String, - certificateColumnName: String, - pathColumnName: String) = PartyAndCertificateColumns(this.varchar(nameColumnName, length = 255), this.publicKey(keyColumnName), - this.certificate(certificateColumnName), this.certificatePath(pathColumnName)) -fun Table.uuidString(name: String) = this.registerColumn(name, UUIDStringColumnType) -fun Table.localDate(name: String) = this.registerColumn(name, LocalDateColumnType) -fun Table.localDateTime(name: String) = this.registerColumn(name, LocalDateTimeColumnType) -fun Table.instant(name: String) = this.registerColumn(name, InstantColumnType) -fun Table.stateRef(txIdColumnName: String, indexColumnName: String) = StateRefColumns(this.secureHash(txIdColumnName), this.integer(indexColumnName)) -fun Table.txnNote(txIdColumnName: String, txnNoteColumnName: String) = TxnNoteColumns(this.secureHash(txIdColumnName), this.text(txnNoteColumnName)) - -/** - * [ColumnType] for marshalling to/from database on behalf of [X509CertificateHolder]. - */ -object X509CertificateColumnType : ColumnType() { - override fun sqlType(): String = "BLOB" - - override fun valueFromDB(value: Any): Any { - val blob = value as JdbcBlob - return X509CertificateHolder(blob.getBytes(0, blob.length().toInt())) - } - - override fun notNullValueToDB(value: Any): Any = (value as X509CertificateHolder).encoded -} - -/** - * [ColumnType] for marshalling to/from database on behalf of [CertPath]. - */ -object CertPathColumnType : ColumnType() { - private val factory = CertificateFactory.getInstance("X.509") - override fun sqlType(): String = "BLOB" - - override fun valueFromDB(value: Any): Any { - val blob = value as JdbcBlob - return factory.generateCertPath(ByteArrayInputStream(blob.getBytes(0, blob.length().toInt()))) - } - - override fun notNullValueToDB(value: Any): Any = (value as CertPath).encoded -} - -/** - * [ColumnType] for marshalling to/from database on behalf of [PublicKey]. - */ -// TODO Rethink how we store CompositeKeys in db. Currently they are stored as Base58 strings and as we don't know the size -// of a CompositeKey they could be CLOB fields. Given the time to fetch these types and that they are unsuitable as table keys, -// having a shorter primary key (such as SHA256 hash or a UUID generated on demand) that references a common composite key table may make more sense. -object PublicKeyColumnType : ColumnType() { - override fun sqlType(): String = "VARCHAR" - - override fun valueFromDB(value: Any): Any = parsePublicKeyBase58(value.toString()) - - override fun notNullValueToDB(value: Any): Any = (value as? PublicKey)?.toBase58String() ?: value -} - -/** - * [ColumnType] for marshalling to/from database on behalf of [SecureHash]. - */ -object SecureHashColumnType : ColumnType() { - override fun sqlType(): String = "VARCHAR(64)" - - override fun valueFromDB(value: Any): Any = SecureHash.parse(value.toString()) - - override fun notNullValueToDB(value: Any): Any = (value as? SecureHash)?.toString() ?: value -} - -/** - * [ColumnType] for marshalling to/from database on behalf of [UUID], always using a string representation. - */ -object UUIDStringColumnType : ColumnType() { - override fun sqlType(): String = "VARCHAR(36)" - - override fun valueFromDB(value: Any): Any = UUID.fromString(value.toString()) - - override fun notNullValueToDB(value: Any): Any = (value as? UUID)?.toString() ?: value -} - -/** - * [ColumnType] for marshalling to/from database on behalf of [java.time.LocalDate]. - */ -object LocalDateColumnType : ColumnType() { - override fun sqlType(): String = "DATE" - - override fun nonNullValueToString(value: Any): String { - if (value is String) return value - - val localDate = when (value) { - is LocalDate -> value - is java.sql.Date -> value.toLocalDate() - is java.sql.Timestamp -> value.toLocalDateTime().toLocalDate() - else -> error("Unexpected value: $value") - } - return "'$localDate'" - } - - override fun valueFromDB(value: Any): Any = when (value) { - is java.sql.Date -> value.toLocalDate() - is java.sql.Timestamp -> value.toLocalDateTime().toLocalDate() - is Long -> LocalDate.from(Instant.ofEpochMilli(value)) - else -> value - } - - override fun notNullValueToDB(value: Any): Any = if (value is LocalDate) { - java.sql.Date(value.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()) - } else value -} - - -/** - * [ColumnType] for marshalling to/from database on behalf of [java.time.LocalDateTime]. - */ -object LocalDateTimeColumnType : ColumnType() { - private val sqlType = DateColumnType(time = true).sqlType() - override fun sqlType(): String = sqlType - - override fun nonNullValueToString(value: Any): String { - if (value is String) return value - - val localDateTime = when (value) { - is LocalDateTime -> value - is java.sql.Date -> value.toLocalDate().atStartOfDay() - is java.sql.Timestamp -> value.toLocalDateTime() - else -> error("Unexpected value: $value") - } - return "'$localDateTime'" - } - - override fun valueFromDB(value: Any): Any = when (value) { - is java.sql.Date -> value.toLocalDate().atStartOfDay() - is java.sql.Timestamp -> value.toLocalDateTime() - is Long -> LocalDateTime.from(Instant.ofEpochMilli(value)) - else -> value - } - - override fun notNullValueToDB(value: Any): Any = if (value is LocalDateTime) { - java.sql.Timestamp(value.toInstant(ZoneOffset.UTC).toEpochMilli()) - } else value -} - - -/** - * [ColumnType] for marshalling to/from database on behalf of [java.time.Instant]. - */ -object InstantColumnType : ColumnType() { - private val sqlType = DateColumnType(time = true).sqlType() - override fun sqlType(): String = sqlType - - override fun nonNullValueToString(value: Any): String { - if (value is String) return value - - val localDateTime = when (value) { - is Instant -> value - is java.sql.Date -> value.toLocalDate().atStartOfDay().toInstant(ZoneOffset.UTC) - is java.sql.Timestamp -> value.toLocalDateTime().toInstant(ZoneOffset.UTC) - else -> error("Unexpected value: $value") - } - return "'$localDateTime'" - } - - override fun valueFromDB(value: Any): Any = when (value) { - is java.sql.Date -> value.toLocalDate().atStartOfDay().toInstant(ZoneOffset.UTC) - is java.sql.Timestamp -> value.toLocalDateTime().toInstant(ZoneOffset.UTC) - is Long -> LocalDateTime.from(Instant.ofEpochMilli(value)).toInstant(ZoneOffset.UTC) - else -> value - } - - override fun notNullValueToDB(value: Any): Any = if (value is Instant) { - java.sql.Timestamp(value.toEpochMilli()) - } else value -} diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt index da64097850..f847080433 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseTransactionManager.kt @@ -32,7 +32,7 @@ class DatabaseTransaction(isolation: Int, val threadLocal: ThreadLocal(tableName: String, - loadOnInit: Boolean = false, - maxBuckets: Int = DEFAULT_MAX_BUCKETS) - : AbstractJDBCHashMap(BlobMapTable(tableName), loadOnInit, maxBuckets) { - - class BlobMapTable(tableName: String) : JDBCHashedTable(tableName) { - val key = blob("key") - val value = blob("value") - } - - override fun keyFromRow(row: ResultRow): K = deserializeFromBlob(row[table.key]) - override fun valueFromRow(row: ResultRow): V = deserializeFromBlob(row[table.value]) - - override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { - insert[table.key] = serializeToBlob(entry.key, finalizables) - } - - override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { - insert[table.value] = serializeToBlob(entry.value, finalizables) - } - -} - -fun bytesToBlob(value: SerializedBytes<*>, finalizables: MutableList<() -> Unit>): Blob { - val blob = DatabaseTransactionManager.current().connection.createBlob() - finalizables += { blob.free() } - blob.setBytes(1, value.bytes) - return blob -} - -fun serializeToBlob(value: Any, finalizables: MutableList<() -> Unit>): Blob = bytesToBlob(value.serialize(context = STORAGE_CONTEXT), finalizables) - -fun bytesFromBlob(blob: Blob): SerializedBytes { - try { - return SerializedBytes(blob.getBytes(0, blob.length().toInt())) - } finally { - blob.free() - } -} - -@Suppress("UNCHECKED_CAST") -fun deserializeFromBlob(blob: Blob): T = bytesFromBlob(blob).deserialize(context = STORAGE_CONTEXT) as T - -/** - * A convenient JDBC table backed hash set with iteration order based on insertion order. - * See [AbstractJDBCHashSet] and [AbstractJDBCHashMap] for further implementation details. - * - * In this subclass, elements are represented by Blobs of Kryo serialized forms of the element objects. - * If you can extend [AbstractJDBCHashSet] and implement less Kryo dependent element mappings then that is - * likely preferrable. - */ -class JDBCHashSet(tableName: String, - loadOnInit: Boolean = false, - maxBuckets: Int = DEFAULT_MAX_BUCKETS) - : AbstractJDBCHashSet(BlobSetTable(tableName), loadOnInit, maxBuckets) { - - class BlobSetTable(tableName: String) : JDBCHashedTable(tableName) { - val key = blob("key") - } - - override fun elementFromRow(row: ResultRow): K = deserializeFromBlob(row[table.key]) - - override fun addElementToInsert(insert: InsertStatement, entry: K, finalizables: MutableList<() -> Unit>) { - insert[table.key] = serializeToBlob(entry, finalizables) - } -} - -/** - * Base class for JDBC backed hash set that delegates to a JDBC backed hash map where the values are all - * [Unit] and not actually persisted. Iteration order is order of insertion. Iterators can remove(). - * - * See [AbstractJDBCHashMap] for implementation details. - */ -abstract class AbstractJDBCHashSet(protected val table: T, - loadOnInit: Boolean = false, - maxBuckets: Int = DEFAULT_MAX_BUCKETS) : MutableSet, AbstractSet() { - protected val innerMap = object : AbstractJDBCHashMap(table, loadOnInit, maxBuckets) { - override fun keyFromRow(row: ResultRow): K = this@AbstractJDBCHashSet.elementFromRow(row) - - // Return constant. - override fun valueFromRow(row: ResultRow) = Unit - - override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) = - this@AbstractJDBCHashSet.addElementToInsert(insert, entry.key, finalizables) - - // No op as not actually persisted. - override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { - } - - } - - override fun add(element: K): Boolean { - if (innerMap.containsKey(element)) { - return false - } else { - innerMap.put(element, Unit) - return true - } - } - - override fun clear() { - innerMap.clear() - } - - override fun iterator(): MutableIterator = innerMap.keys.iterator() - - override fun remove(element: K): Boolean = (innerMap.remove(element) != null) - - override val size: Int - get() = innerMap.size - - override fun contains(element: K): Boolean = innerMap.containsKey(element) - - override fun isEmpty(): Boolean = innerMap.isEmpty() - - /** - * Implementation should return the element object marshalled from the database table row. - * - * See example implementations in [JDBCHashSet]. - */ - protected abstract fun elementFromRow(row: ResultRow): K - - /** - * Implementation should marshall the element to the insert statement. - * - * If some cleanup is required after the insert statement is executed, such as closing a Blob, then add a closure - * to the finalizables to do so. - * - * See example implementations in [JDBCHashSet]. - */ - protected abstract fun addElementToInsert(insert: InsertStatement, entry: K, finalizables: MutableList<() -> Unit>) -} - -/** - * A base class for a JDBC table backed hash map that iterates in insertion order by using - * an ever increasing sequence number on entries. Iterators supports remove() but entries are not really mutable and - * do not support setValue() method from [MutableMap.MutableEntry]. - * - * You should only use keys that have overridden [Object.hashCode] and that have a good hash code distribution. Beware - * changing the hashCode() implementation once objects have been persisted. A process to re-hash the entries persisted - * would be necessary if you do this. - * - * Subclasses must provide their own mapping to and from keys/values and the database table columns, but there are - * inherited columns that all tables must provide to support iteration order and hashing. - * - * The map operates in one of two modes. - * 1. loadOnInit=true where the entire table is loaded into memory in the constructor and all entries remain in memory, - * with only writes needing to perform database access. - * 2. loadOnInit=false where all entries with the same key hash code are loaded from the database on demand when accessed - * via any method other than via keys/values/entries properties, and thus the whole map is not loaded into memory. The number - * of entries retained in memory is controlled indirectly by an LRU algorithm (courtesy of [LinkedHashMap]) and a maximum - * number of hash "buckets", where one bucket represents all entries with the same hash code. There is a default value - * for maximum buckets. - * - * All operations require a [transaction] to be started. - * - * The keys/values/entries collections are really designed just for iterating and other uses might turn out to be - * costly in terms of performance. Beware when loadOnInit=true, the iterator first sorts the entries which could be - * costly too. - * - * This class is *not* thread safe. - * - * TODO: consider caching size once calculated for the first time. - * TODO: buckets just use a list and so are vulnerable to poor hash code implementations with collisions. - * TODO: if iterators are used extensively when loadOnInit=true, consider maintaining a collection of keys in iteration order to avoid sorting each time. - * TODO: revisit whether we need the loadOnInit==true functionality and remove if not. - */ -abstract class AbstractJDBCHashMap(val table: T, - val loadOnInit: Boolean = false, - val maxBuckets: Int = DEFAULT_MAX_BUCKETS) : MutableMap, AbstractMap() { - - companion object { - protected val log = loggerFor>() - - private const val INITIAL_CAPACITY: Int = 16 - private const val LOAD_FACTOR: Float = 0.75f - } - - // Hash code -> entries mapping. - // When loadOnInit = false, size will be limited to maxBuckets entries (which are hash buckets) and map maintains access order rather than insertion order. - private val buckets = object : LinkedHashMap>>(INITIAL_CAPACITY, LOAD_FACTOR, !loadOnInit) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry>>?): Boolean { - return !loadOnInit && size > maxBuckets - } - } - - init { - check(maxBuckets > 0) { "The maximum number of buckets to retain in memory must be a positive integer." } - // TODO: Move this to schema version managment tool. - createTablesIfNecessary() - if (loadOnInit) { - log.trace { "Loading all entries on init for ${table.tableName}" } - val elapsedMillis = measureTimeMillis { - // load from database. - table.selectAll().map { - val entry = createEntry(it) - val bucket = getBucket(entry.key) - bucket.add(entry) - } - } - log.trace { "Loaded $size entries on init for ${table.tableName} in $elapsedMillis millis." } - } - } - - private fun createTablesIfNecessary() { - SchemaUtils.create(table) - } - - override fun isEmpty(): Boolean { - for (bucket in buckets.values) { - if (!bucket.isEmpty()) { - return false - } - } - return size == 0 - } - - override fun remove(key: K): V? { - val bucket = getBucket(key) - var removed: V? = null - buckets.computeIfPresent(key.hashCode()) { _, value -> - for (entry in value) { - if (entry.key == key) { - removed = entry.value - bucket.remove(entry) - deleteRecord(entry) - break - } - } - value - } - return removed - } - - override fun containsKey(key: K): Boolean = (get(key) != null) - - // We haven't implemented setValue. We could implement if necessary. - // Make sure to remove the relevant suppressed tests in JDBCHashMapTestSuite.createMapTestSuite if this is implemented. - private class NotReallyMutableEntry(key: K, value: V, val seqNo: Int) : AbstractMap.SimpleImmutableEntry(key, value), MutableMap.MutableEntry { - override fun setValue(newValue: V): V { - throw UnsupportedOperationException("Not really mutable. Implement if really required.") - } - } - - private inner class EntryIterator : MutableIterator> { - private val iterator = if (loadOnInit) { - buckets.values.flatten().sortedBy { it.seqNo }.iterator() - } else { - // This uses a Sequence to make the mapping lazy. - table.selectAll().orderBy(table.seqNo).asSequence().map { - val bucket = buckets[it[table.keyHash]] - if (bucket != null) { - val seqNo = it[table.seqNo] - for (entry in bucket) { - if (entry.seqNo == seqNo) { - return@map entry - } - } - } - return@map createEntry(it) - }.iterator() - } - private var current: MutableMap.MutableEntry? = null - - override fun hasNext(): Boolean = iterator.hasNext() - - override fun next(): MutableMap.MutableEntry { - val extractedNext = iterator.next() - current = extractedNext - return extractedNext - } - - override fun remove() { - val savedCurrent = current ?: throw IllegalStateException("Not called next() yet or already removed.") - current = null - remove(savedCurrent.key) - } - } - - override val keys: MutableSet get() { - return object : AbstractSet() { - override val size: Int get() = this@AbstractJDBCHashMap.size - override fun iterator(): MutableIterator { - return object : MutableIterator { - private val entryIterator = EntryIterator() - - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): K = entryIterator.next().key - override fun remove() { - entryIterator.remove() - } - } - } - } - } - - override val values: MutableCollection get() { - return object : AbstractCollection() { - override val size: Int get() = this@AbstractJDBCHashMap.size - override fun iterator(): MutableIterator { - return object : MutableIterator { - private val entryIterator = EntryIterator() - - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): V = entryIterator.next().value - override fun remove() { - entryIterator.remove() - } - } - } - } - } - - override val entries: MutableSet> get() { - return object : AbstractSet>() { - override val size: Int get() = this@AbstractJDBCHashMap.size - override fun iterator(): MutableIterator> { - return object : MutableIterator> { - private val entryIterator = EntryIterator() - - override fun hasNext(): Boolean = entryIterator.hasNext() - override fun next(): MutableMap.MutableEntry = entryIterator.next() - override fun remove() { - entryIterator.remove() - } - } - } - } - } - - override fun put(key: K, value: V): V? { - var oldValue: V? = null - var oldSeqNo: Int? = null - getBucket(key) - buckets.compute(key.hashCode()) { _, list -> - val newList = list ?: newBucket() - val iterator = newList.listIterator() - while (iterator.hasNext()) { - val entry = iterator.next() - if (entry.key == key) { - oldValue = entry.value - oldSeqNo = entry.seqNo - iterator.remove() - deleteRecord(entry) - break - } - } - val seqNo = addRecord(key, value, oldSeqNo) - val newEntry = NotReallyMutableEntry(key, value, seqNo) - newList.add(newEntry) - newList - } - return oldValue - } - - override fun containsValue(value: V): Boolean { - for (storedValue in values) { - if (storedValue == value) { - return true - } - } - return false - } - - override val size: Int get() { - return if (loadOnInit) { - buckets.values.map { it.size }.sum() - } else { - table.slice(table.seqNo).selectAll().count() - } - } - - override fun clear() { - if (!loadOnInit || !isEmpty()) { - table.deleteAll() - buckets.clear() - } - } - - override fun get(key: K): V? { - for ((entryKey, value) in getBucket(key)) { - if (entryKey == key) { - return value - } - } - return null - } - - private fun getBucket(key: Any): MutableList> { - return buckets.computeIfAbsent(key.hashCode()) { _ -> - if (!loadOnInit) { - loadBucket(key.hashCode()) - } else { - newBucket() - } - } - } - - private fun newBucket(): MutableList> = mutableListOf() - - private fun loadBucket(hashCode: Int): MutableList> { - return table.select { table.keyHash.eq(hashCode) }.map { - createEntry(it) - }.toMutableList>() - } - - /** - * Implementation should return the key object marshalled from the database table row. - * - * See example implementations in [JDBCHashMap]. - */ - protected abstract fun keyFromRow(row: ResultRow): K - - /** - * Implementation should return the value object marshalled from the database table row. - * - * See example implementations in [JDBCHashMap]. - */ - protected abstract fun valueFromRow(row: ResultRow): V - - /** - * Implementation should marshall the key to the insert statement. - * - * If some cleanup is required after the insert statement is executed, such as closing a Blob, then add a closure - * to the finalizables to do so. - * - * See example implementations in [JDBCHashMap]. - */ - protected abstract fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) - - /** - * Implementation should marshall the value to the insert statement. - * - * If some cleanup is required after the insert statement is executed, such as closing a Blob, then add a closure - * to the finalizables to do so. - * - * See example implementations in [JDBCHashMap]. - */ - protected abstract fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) - - private fun createEntry(it: ResultRow) = NotReallyMutableEntry(keyFromRow(it), valueFromRow(it), it[table.seqNo]) - - private fun deleteRecord(entry: NotReallyMutableEntry) { - table.deleteWhere { - table.seqNo eq entry.seqNo - } - } - - private fun addRecord(key: K, value: V, oldSeqNo: Int?): Int { - val finalizables = mutableListOf<() -> Unit>() - try { - return table.insert { - it[keyHash] = key.hashCode() - val entry = SimpleEntry(key, value) - addKeyToInsert(it, entry, finalizables) - addValueToInsert(it, entry, finalizables) - if (oldSeqNo != null) { - it[seqNo] = oldSeqNo - it.generatedKey = oldSeqNo - } - } get table.seqNo - } finally { - finalizables.forEach { it() } - } - } -} - -open class JDBCHashedTable(tableName: String) : Table(tableName) { - val keyHash = integer("key_hash").index() - val seqNo = integer("seq_no").autoIncrement().index().primaryKey() -} diff --git a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt index b429f2e09e..fbe2947a70 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/KeyStoreUtilities.kt @@ -1,8 +1,11 @@ package net.corda.node.utilities -import net.corda.core.crypto.* +import net.corda.core.crypto.CertificateAndKeyPair +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.cert import net.corda.core.internal.exists import net.corda.core.internal.read +import net.corda.core.internal.toX509CertHolder import net.corda.core.internal.write import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder @@ -145,8 +148,8 @@ fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): Certi * @return The X509Certificate found in the KeyStore under the specified alias. */ fun KeyStore.getX509Certificate(alias: String): X509CertificateHolder { - val encoded = getCertificate(alias)?.encoded ?: throw IllegalArgumentException("No certificate under alias \"$alias\"") - return X509CertificateHolder(encoded) + val certificate = getCertificate(alias) ?: throw IllegalArgumentException("No certificate under alias \"$alias\"") + return certificate.toX509CertHolder() } /** @@ -206,5 +209,5 @@ class KeyStoreWrapper(private val storePath: Path, private val storePassword: St fun getCertificate(alias: String): Certificate = keyStore.getCertificate(alias) - fun certificateAndKeyPair(alias: String) = keyStore.getCertificateAndKeyPair(alias, storePassword) + fun certificateAndKeyPair(alias: String): CertificateAndKeyPair = keyStore.getCertificateAndKeyPair(alias, storePassword) } diff --git a/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt new file mode 100644 index 0000000000..44caaa9adb --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/NonInvalidatingUnboundCache.kt @@ -0,0 +1,35 @@ +package net.corda.node.utilities + +import com.google.common.cache.* +import com.google.common.util.concurrent.ListenableFuture + + +class NonInvalidatingUnboundCache private constructor( + val cache: LoadingCache +): LoadingCache by cache { + + constructor(concurrencyLevel: Int, loadFunction: (K) -> V, removalListener: RemovalListener = RemovalListener {}, + keysToPreload: () -> Iterable = { emptyList() } ) : + this(buildCache(concurrencyLevel, loadFunction, removalListener, keysToPreload)) + + private companion object { + private fun buildCache(concurrencyLevel: Int, loadFunction: (K) -> V, removalListener: RemovalListener, + keysToPreload: () -> Iterable): LoadingCache { + val builder = CacheBuilder.newBuilder().concurrencyLevel(concurrencyLevel).removalListener(removalListener) + return builder.build(NonInvalidatingCacheLoader(loadFunction)).apply { + getAll(keysToPreload()) + } + } + } + + // TODO look into overriding loadAll() if we ever use it + private class NonInvalidatingCacheLoader(val loadFunction: (K) -> V) : CacheLoader() { + override fun reload(key: K, oldValue: V): ListenableFuture { + throw IllegalStateException("Non invalidating cache refreshed") + } + override fun load(key: K) = loadFunction(key) + override fun loadAll(keys: Iterable): MutableMap { + return super.loadAll(keys) + } + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt new file mode 100644 index 0000000000..2d43aae33c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/utilities/PersistentMap.kt @@ -0,0 +1,259 @@ +package net.corda.node.utilities + + +import com.google.common.cache.RemovalCause +import com.google.common.cache.RemovalListener +import com.google.common.cache.RemovalNotification +import net.corda.core.utilities.loggerFor +import java.util.* + + +/** + * Implements an unbound caching layer on top of a table accessed via Hibernate mapping. + */ +class PersistentMap ( + val toPersistentEntityKey: (K) -> EK, + val fromPersistentEntity: (E) -> Pair, + val toPersistentEntity: (key: K, value: V) -> E, + val persistentEntityClass: Class +) : MutableMap, AbstractMap() { + + private companion object { + val log = loggerFor>() + } + + private val cache = NonInvalidatingUnboundCache( + concurrencyLevel = 8, + loadFunction = { key -> Optional.ofNullable(loadValue(key)) }, + removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass) + ).apply { + //preload to allow all() to take data only from the cache (cache is unbound) + val session = DatabaseTransactionManager.current().session + val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) + criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) + getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) + } + + class ExplicitRemoval(private val toPersistentEntityKey: (K) -> EK, private val persistentEntityClass: Class): RemovalListener { + override fun onRemoval(notification: RemovalNotification?) { + when (notification?.cause) { + RemovalCause.EXPLICIT -> { + val session = DatabaseTransactionManager.current().session + val elem = session.find(persistentEntityClass, toPersistentEntityKey(notification.key)) + if (elem != null) { + session.remove(elem) + } + } + RemovalCause.EXPIRED, RemovalCause.SIZE, RemovalCause.COLLECTED -> { + log.error("Entry was removed from cache!!!") + } + RemovalCause.REPLACED -> {} + } + } + } + + override operator fun get(key: K): V? { + return cache.get(key).orElse(null) + } + + fun all(): Sequence> { + return cache.asMap().asSequence().map { Pair(it.key, it.value.get()) } + } + + override val size get() = cache.size().toInt() + + private tailrec fun set(key: K, value: V, logWarning: Boolean = true, store: (K,V) -> V?, replace: (K, V) -> Unit) : Boolean { + var insertionAttempt = false + var isUnique = true + val existingInCache = cache.get(key) { // Thread safe, if multiple threads may wait until the first one has loaded. + insertionAttempt = true + // Value wasn't in the cache and wasn't in DB (because the cache is unbound). + // Store the value, depending on store implementation this may replace existing entry in DB. + store(key, value) + Optional.of(value) + } + if (!insertionAttempt) { + if (existingInCache.isPresent) { + // Key already exists in cache, store the new value in the DB (depends on tore implementation) and refresh cache. + isUnique = false + replace(key, value) + } else { + // This happens when the key was queried before with no value associated. We invalidate the cached null + // value and recursively call set again. This is to avoid race conditions where another thread queries after + // the invalidate but before the set. + cache.invalidate(key) + return set(key, value, logWarning, store, replace) + } + } + if (logWarning && !isUnique) { + log.warn("Double insert in ${this.javaClass.name} for entity class $persistentEntityClass key $key, not inserting the second time") + } + return isUnique + } + + /** + * Associates the specified value with the specified key in this map and persists it. + * WARNING! If the map previously contained a mapping for the key, the behaviour is unpredictable and may throw an error from the underlying storage. + */ + operator fun set(key: K, value: V) = + set(key, value, + logWarning = false, + store = { k: K, v: V -> + DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + null + }, + replace = { _: K, _: V -> Unit } + ) + + /** + * Associates the specified value with the specified key in this map and persists it. + * WARNING! If the map previously contained a mapping for the key, the old value is not replaced. + * @return true if added key was unique, otherwise false + */ + fun addWithDuplicatesAllowed(key: K, value: V) = + set(key, value, + store = { k, v -> + val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(k)) + if (existingEntry == null) { + DatabaseTransactionManager.current().session.save(toPersistentEntity(k, v)) + null + } else { + fromPersistentEntity(existingEntry).second + } + }, + replace = { _: K, _: V -> Unit } + ) + + /** + * Associates the specified value with the specified key in this map and persists it. + * @return true if added key was unique, otherwise false + */ + private fun addWithDuplicatesReplaced(key: K, value: V) = + set(key, value, + logWarning = false, + store = { k: K, v: V -> merge(k, v) }, + replace = { k: K, v: V -> replaceValue(k, v) } + ) + + private fun replaceValue(key: K, value: V) { + synchronized(this) { + merge(key, value) + cache.put(key, Optional.of(value)) + } + } + + private fun merge(key: K, value: V): V? { + val existingEntry = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + return if (existingEntry != null) { + DatabaseTransactionManager.current().session.merge(toPersistentEntity(key,value)) + fromPersistentEntity(existingEntry).second + } else { + DatabaseTransactionManager.current().session.save(toPersistentEntity(key,value)) + null + } + } + + private fun loadValue(key: K): V? { + val result = DatabaseTransactionManager.current().session.find(persistentEntityClass, toPersistentEntityKey(key)) + return result?.let(fromPersistentEntity)?.second + } + + /** + * Removes the mapping for the specified key from this map and underlying storage if present. + */ + override fun remove(key: K): V? { + val result = cache.get(key).orElse(null) + cache.invalidate(key) + return result + } + + private class NotReallyMutableEntry(key: K, value: V) : AbstractMap.SimpleImmutableEntry(key, value), MutableMap.MutableEntry { + override fun setValue(newValue: V): V { + throw UnsupportedOperationException("Not really mutable. Implement if really required.") + } + } + + private inner class EntryIterator : MutableIterator> { + private val iterator = all().map { NotReallyMutableEntry(it.first, it.second) }.iterator() + + private var current: MutableMap.MutableEntry? = null + + override fun hasNext(): Boolean = iterator.hasNext() + + override fun next(): MutableMap.MutableEntry { + val extractedNext = iterator.next() + current = extractedNext + return extractedNext + } + + override fun remove() { + val savedCurrent = current ?: throw IllegalStateException("Not called next() yet or already removed.") + current = null + remove(savedCurrent.key) + } + } + + override val keys: MutableSet get() { + return object : AbstractSet() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val entryIterator = EntryIterator() + + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): K = entryIterator.next().key + override fun remove() { + entryIterator.remove() + } + } + } + } + } + + override val values: MutableCollection get() { + return object : AbstractCollection() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator { + return object : MutableIterator { + private val entryIterator = EntryIterator() + + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): V = entryIterator.next().value + override fun remove() { + entryIterator.remove() + } + } + } + } + } + + override val entries: MutableSet> get() { + return object : AbstractSet>() { + override val size: Int get() = this@PersistentMap.size + override fun iterator(): MutableIterator> { + return object : MutableIterator> { + private val entryIterator = EntryIterator() + + override fun hasNext(): Boolean = entryIterator.hasNext() + override fun next(): MutableMap.MutableEntry = entryIterator.next() + override fun remove() { + entryIterator.remove() + } + } + } + } + } + + override fun put(key: K, value: V): V? { + val old = cache.get(key) + addWithDuplicatesReplaced(key, value) + return old.orElse(null) + } + + fun load() { + val session = DatabaseTransactionManager.current().session + val criteriaQuery = session.criteriaBuilder.createQuery(persistentEntityClass) + criteriaQuery.select(criteriaQuery.from(persistentEntityClass)) + cache.getAll(session.createQuery(criteriaQuery).resultList.map { e -> fromPersistentEntity(e as E).first }.asIterable()) + } +} diff --git a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt index 53ed5580eb..f10261826f 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/X509Utilities.kt @@ -211,7 +211,7 @@ object X509Utilities { nameConstraints: NameConstraints? = null): X509CertificateHolder { val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) - val provider = Crypto.providerMap[signatureScheme.providerName] + val provider = Crypto.findProvider(signatureScheme.providerName) val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) @@ -225,7 +225,7 @@ object X509Utilities { * Create certificate signing request using provided information. */ fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme): PKCS10CertificationRequest { - val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.providerMap[signatureScheme.providerName]) + val signer = ContentSignerBuilder.build(signatureScheme, keyPair.private, Crypto.findProvider(signatureScheme.providerName)) return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer) } diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 1d2cbb9995..455f22ab3a 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -2,12 +2,12 @@ package net.corda.node.services.vault; import com.google.common.collect.ImmutableSet; import kotlin.Pair; -import net.corda.contracts.DealState; -import net.corda.contracts.asset.Cash; +import kotlin.Triple; import net.corda.core.contracts.*; import net.corda.core.crypto.EncodingUtils; import net.corda.core.identity.AbstractParty; import net.corda.core.messaging.DataFeed; +import net.corda.core.node.services.IdentityService; import net.corda.core.node.services.Vault; import net.corda.core.node.services.VaultQueryException; import net.corda.core.node.services.VaultQueryService; @@ -15,9 +15,14 @@ import net.corda.core.node.services.vault.*; import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria; import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; +import net.corda.core.schemas.MappedSchema; import net.corda.core.utilities.OpaqueBytes; +import net.corda.finance.contracts.DealState; +import net.corda.finance.contracts.asset.Cash; +import net.corda.finance.contracts.asset.CashUtilities; +import net.corda.finance.schemas.CashSchemaV1; import net.corda.node.utilities.CordaPersistence; -import net.corda.schemas.CashSchemaV1; +import net.corda.node.utilities.DatabaseTransaction; import net.corda.testing.TestConstants; import net.corda.testing.TestDependencyInjectionBase; import net.corda.testing.contracts.DummyLinearContract; @@ -36,14 +41,16 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; -import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM; import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE; import static net.corda.core.utilities.ByteArrays.toHexString; +import static net.corda.finance.contracts.asset.CashUtilities.getDUMMY_CASH_ISSUER; +import static net.corda.finance.contracts.asset.CashUtilities.getDUMMY_CASH_ISSUER_KEY; import static net.corda.testing.CoreTestUtils.*; -import static net.corda.testing.TestConstants.*; +import static net.corda.testing.TestConstants.getDUMMY_NOTARY; +import static net.corda.testing.TestConstants.getDUMMY_NOTARY_KEY; import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices; +import static net.corda.testing.node.MockServicesKt.makeTestIdentityService; import static org.assertj.core.api.Assertions.assertThat; public class VaultQueryJavaTests extends TestDependencyInjectionBase { @@ -58,7 +65,11 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { ArrayList keys = new ArrayList<>(); keys.add(getMEGA_CORP_KEY()); keys.add(getDUMMY_NOTARY_KEY()); - Pair databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys); + Set requiredSchemas = new HashSet<>(); + requiredSchemas.add(CashSchemaV1.INSTANCE); + IdentityService identitySvc = makeTestIdentityService(); + @SuppressWarnings("unchecked") + Pair databaseAndServices = makeTestDatabaseAndMockServices(requiredSchemas, keys, () -> identitySvc); issuerServices = new MockServices(getDUMMY_CASH_ISSUER_KEY(), getBOC_KEY()); database = databaseAndServices.getFirst(); services = databaseAndServices.getSecond(); @@ -75,15 +86,16 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { */ /** - * Static queryBy() tests + * Static queryBy() tests */ @Test public void unconsumedLinearStates() throws VaultQueryException { database.transaction(tx -> { - VaultFiller.fillWithSomeTestLinearStates(services, 3); - + return tx; + }); + database.transaction(tx -> { // DOCSTART VaultJavaQueryExample0 Vault.Page results = vaultQuerySvc.queryBy(LinearState.class); // DOCEND VaultJavaQueryExample0 @@ -96,11 +108,12 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Test public void unconsumedStatesForStateRefsSortedByTxnId() { + Vault issuedStates = + database.transaction(tx -> { + VaultFiller.fillWithSomeTestLinearStates(services, 8); + return VaultFiller.fillWithSomeTestLinearStates(services, 2); + }); database.transaction(tx -> { - - VaultFiller.fillWithSomeTestLinearStates(services, 8); - Vault issuedStates = VaultFiller.fillWithSomeTestLinearStates(services, 2); - Stream stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef); List stateRefs = stateRefsStream.collect(Collectors.toList()); @@ -121,23 +134,25 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Test public void consumedCashStates() { + Amount amount = new Amount<>(100, Currency.getInstance("USD")); database.transaction(tx -> { - - Amount amount = new Amount<>(100, Currency.getInstance("USD")); - VaultFiller.fillWithSomeTestCash(services, - new Amount<>(100, Currency.getInstance("USD")), - issuerServices, - TestConstants.getDUMMY_NOTARY(), - 3, - 3, - new Random(), - new OpaqueBytes("1".getBytes()), - null, - getDUMMY_CASH_ISSUER()); - + new Amount(100, Currency.getInstance("USD")), + issuerServices, + TestConstants.getDUMMY_NOTARY(), + 3, + 3, + new Random(), + new OpaqueBytes("1".getBytes()), + null, + CashUtilities.getDUMMY_CASH_ISSUER()); + return tx; + }); + database.transaction(tx -> { VaultFiller.consumeCash(services, amount, getDUMMY_NOTARY()); - + return tx; + }); + database.transaction(tx -> { // DOCSTART VaultJavaQueryExample1 VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.CONSUMED); Vault.Page results = vaultQuerySvc.queryBy(Cash.State.class, criteria); @@ -151,19 +166,24 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Test public void consumedDealStatesPagedSorted() throws VaultQueryException { + List dealIds = Arrays.asList("123", "456", "789"); + @SuppressWarnings("unchecked") + Triple, UniqueIdentifier, Vault> ids = + database.transaction((DatabaseTransaction tx) -> { + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); + StateAndRef linearState = states.getStates().iterator().next(); + UniqueIdentifier uid = linearState.component1().getData().getLinearId(); + + Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); + return new Triple(linearState,uid,dealStates); + }); database.transaction(tx -> { - - Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); - StateAndRef linearState = states.getStates().iterator().next(); - UniqueIdentifier uid = linearState.component1().getData().getLinearId(); - - List dealIds = Arrays.asList("123", "456", "789"); - Vault dealStates = VaultFiller.fillWithSomeTestDeals(services, dealIds); - // consume states - VaultFiller.consumeDeals(services, (List>) dealStates.getStates(), getDUMMY_NOTARY()); - VaultFiller.consumeLinearStates(services, Collections.singletonList(linearState), getDUMMY_NOTARY()); - + VaultFiller.consumeDeals(services, (List>) ids.getThird().getStates(), getDUMMY_NOTARY()); + VaultFiller.consumeLinearStates(services, Collections.singletonList(ids.getFirst()), getDUMMY_NOTARY()); + return tx; + }); + database.transaction(tx -> { // DOCSTART VaultJavaQueryExample2 Vault.StateStatus status = Vault.StateStatus.CONSUMED; @SuppressWarnings("unchecked") @@ -171,14 +191,14 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { QueryCriteria vaultCriteria = new VaultQueryCriteria(status, contractStateTypes); - List linearIds = Collections.singletonList(uid.getId()); - QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds); + List linearIds = Collections.singletonList(ids.getSecond()); + QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds, Vault.StateStatus.UNCONSUMED, null); QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds); QueryCriteria compositeCriteria1 = dealCriteriaAll.or(linearCriteriaAll); - QueryCriteria compositeCriteria2 = vaultCriteria.and(compositeCriteria1); + QueryCriteria compositeCriteria2 = compositeCriteria1.and(vaultCriteria); - PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); + PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); Vault.Page results = vaultQuerySvc.queryBy(LinearState.class, compositeCriteria2, pageSpec, sorting); @@ -204,7 +224,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { VaultFiller.fillWithSomeTestCash(services, dollars100, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); VaultFiller.fillWithSomeTestCash(services, dollars10, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); VaultFiller.fillWithSomeTestCash(services, dollars1, issuerServices, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); - + return tx; + }); + database.transaction(tx -> { try { // DOCSTART VaultJavaQueryExample3 QueryCriteria generalCriteria = new VaultQueryCriteria(Vault.StateStatus.ALL); @@ -227,12 +249,12 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { } catch (NoSuchFieldException e) { e.printStackTrace(); } - return tx; + return tx; }); } /** - * Dynamic trackBy() tests + * Dynamic trackBy() tests */ @Test @@ -248,7 +270,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); - + return tx; + }); + database.transaction(tx -> { // DOCSTART VaultJavaQueryExample4 @SuppressWarnings("unchecked") Set> contractStateTypes = new HashSet(Collections.singletonList(Cash.State.class)); @@ -268,27 +292,29 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @Test public void trackDealStatesPagedSorted() { + List dealIds = Arrays.asList("123", "456", "789"); + UniqueIdentifier uid = + database.transaction(tx -> { + Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); + UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId(); + + VaultFiller.fillWithSomeTestDeals(services, dealIds); + return _uid; + }); database.transaction(tx -> { - - Vault states = VaultFiller.fillWithSomeTestLinearStates(services, 10, null); - UniqueIdentifier uid = states.getStates().iterator().next().component1().getData().getLinearId(); - - List dealIds = Arrays.asList("123", "456", "789"); - VaultFiller.fillWithSomeTestDeals(services, dealIds); - // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") Set> contractStateTypes = new HashSet(Arrays.asList(DealState.class, LinearState.class)); QueryCriteria vaultCriteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, contractStateTypes); - List linearIds = Collections.singletonList(uid.getId()); + List linearIds = Collections.singletonList(uid); List dealParty = Collections.singletonList(getMEGA_CORP()); QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds); - QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null); + QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, Vault.StateStatus.UNCONSUMED, null); QueryCriteria dealOrLinearIdCriteria = dealCriteria.or(linearCriteria); QueryCriteria compositeCriteria = dealOrLinearIdCriteria.and(vaultCriteria); - PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); + PageSpecification pageSpec = new PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE); Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC); Sort sorting = new Sort(ImmutableSet.of(sortByUid)); DataFeed, Vault.Update> results = vaultQuerySvc.trackBy(ContractState.class, compositeCriteria, pageSpec, sorting); @@ -303,7 +329,7 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { } /** - * Aggregation Functions + * Aggregation Functions */ @Test @@ -323,6 +349,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { VaultFiller.fillWithSomeTestCash(services, pounds, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); VaultFiller.fillWithSomeTestCash(services, swissfrancs, issuerServices, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); + return tx; + }); + database.transaction(tx -> { try { // DOCSTART VaultJavaQueryExample21 Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); @@ -368,6 +397,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { VaultFiller.fillWithSomeTestCash(services, pounds, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); VaultFiller.fillWithSomeTestCash(services, swissfrancs, issuerServices, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); + return tx; + }); + database.transaction(tx -> { try { // DOCSTART VaultJavaQueryExample22 Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); @@ -426,7 +458,6 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { @SuppressWarnings("unchecked") public void aggregateFunctionsSumByIssuerAndCurrencyAndSortByAggregateSum() { database.transaction(tx -> { - Amount dollars100 = new Amount<>(100, Currency.getInstance("USD")); Amount dollars200 = new Amount<>(200, Currency.getInstance("USD")); Amount pounds300 = new Amount<>(300, Currency.getInstance("GBP")); @@ -437,6 +468,9 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase { VaultFiller.fillWithSomeTestCash(services, pounds300, issuerServices, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER()); VaultFiller.fillWithSomeTestCash(services, pounds400, issuerServices, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes()))); + return tx; + }); + database.transaction(tx -> { try { // DOCSTART VaultJavaQueryExample23 Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies"); diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 6d2d235610..158a8ce8b3 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -1,8 +1,9 @@ package net.corda.node import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.Issued import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogic @@ -10,17 +11,21 @@ import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.* import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.getOrThrow import net.corda.core.node.services.queryBy +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.GBP +import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.CordaRPCOpsImpl import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcContext import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.PermissionException import net.corda.nodeapi.User @@ -90,9 +95,8 @@ class CordaRPCOpsImplTest { } // Tell the monitoring service node to issue some cash - val anonymous = false val recipient = aliceNode.info.legalIdentity - val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity, anonymous) + val result = rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, notaryNode.info.notaryIdentity) mockNet.runNetwork() var issueSmId: StateMachineRunId? = null @@ -131,9 +135,7 @@ class CordaRPCOpsImplTest { val result = rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes(ByteArray(1, { 1 })), - aliceNode.info.legalIdentity, - notaryNode.info.notaryIdentity, - false + notaryNode.info.notaryIdentity ) mockNet.runNetwork() @@ -208,13 +210,7 @@ class CordaRPCOpsImplTest { fun `cash command by user not permissioned for cash`() { CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = emptySet()))) assertThatExceptionOfType(PermissionException::class.java).isThrownBy { - rpc.startFlow(::CashIssueFlow, - Amount(100, USD), - OpaqueBytes(ByteArray(1, { 1 })), - aliceNode.info.legalIdentity, - notaryNode.info.notaryIdentity, - false - ) + rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), notaryNode.info.notaryIdentity) } } diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index 4838ee78b5..6a70ea30a0 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -4,11 +4,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.Amount import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowContext -import net.corda.core.flows.FlowInitiator -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStackSnapshot -import net.corda.core.flows.StateMachineRunId +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.node.ServiceHub @@ -93,21 +89,9 @@ class InteractiveShellTest { override val id: StateMachineRunId get() = throw UnsupportedOperationException() override val resultFuture: CordaFuture get() = throw UnsupportedOperationException() override val flowInitiator: FlowInitiator get() = throw UnsupportedOperationException() - - override fun checkFlowPermission(permissionName: String, extraAuditData: Map) { - // Do nothing - } - - override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) { - // Do nothing - } - - override fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? { - return null - } - - override fun persistFlowStackSnapshot(flowClass: Class<*>) { - // Do nothing - } + override fun checkFlowPermission(permissionName: String, extraAuditData: Map) = Unit + override fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) = Unit + override fun flowStackSnapshot(flowClass: Class>): FlowStackSnapshot? = null + override fun persistFlowStackSnapshot(flowClass: Class>) = Unit } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index 7dbce0a062..01f04ada97 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -1,14 +1,13 @@ package net.corda.node.messaging import net.corda.core.node.services.ServiceInfo -import net.corda.node.services.api.DEFAULT_SESSION_ID import net.corda.node.services.messaging.Message import net.corda.node.services.messaging.TopicStringValidator import net.corda.node.services.messaging.createMessage import net.corda.node.services.network.NetworkMapService import net.corda.testing.node.MockNetwork -import org.junit.After import net.corda.testing.resetTestSerialization +import org.junit.After import org.junit.Before import org.junit.Test import java.util.* @@ -70,7 +69,7 @@ class InMemoryMessagingTests { } // Node 1 sends a message and it should end up in finalDelivery, after we run the network - node1.network.send(node1.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), node2.network.myAddress) + node1.network.send(node1.network.createMessage("test.topic", data = bits), node2.network.myAddress) mockNet.runNetwork(rounds = 1) @@ -87,7 +86,7 @@ class InMemoryMessagingTests { var counter = 0 listOf(node1, node2, node3).forEach { it.network.addMessageHandler { _, _ -> counter++ } } - node1.network.send(node2.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), mockNet.messagingNetwork.everyoneOnline) + node1.network.send(node2.network.createMessage("test.topic", data = bits), mockNet.messagingNetwork.everyoneOnline) mockNet.runNetwork(rounds = 1) assertEquals(3, counter) } @@ -106,8 +105,8 @@ class InMemoryMessagingTests { received++ } - val invalidMessage = node2.network.createMessage("invalid_message", DEFAULT_SESSION_ID, ByteArray(0)) - val validMessage = node2.network.createMessage("valid_message", DEFAULT_SESSION_ID, ByteArray(0)) + val invalidMessage = node2.network.createMessage("invalid_message", data = ByteArray(0)) + val validMessage = node2.network.createMessage("valid_message", data = ByteArray(0)) node2.network.send(invalidMessage, node1.network.myAddress) mockNet.runNetwork() assertEquals(0, received) @@ -118,8 +117,8 @@ class InMemoryMessagingTests { // Here's the core of the test; previously the unhandled message would cause runNetwork() to abort early, so // this would fail. Make fresh messages to stop duplicate uniqueMessageId causing drops - val invalidMessage2 = node2.network.createMessage("invalid_message", DEFAULT_SESSION_ID, ByteArray(0)) - val validMessage2 = node2.network.createMessage("valid_message", DEFAULT_SESSION_ID, ByteArray(0)) + val invalidMessage2 = node2.network.createMessage("invalid_message", data = ByteArray(0)) + val validMessage2 = node2.network.createMessage("valid_message", data = ByteArray(0)) node2.network.send(invalidMessage2, node1.network.myAddress) node2.network.send(validMessage2, node1.network.myAddress) mockNet.runNetwork() diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index 8f415beb52..55b1ff406b 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -1,11 +1,6 @@ package net.corda.node.messaging import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.CommercialPaper -import net.corda.contracts.asset.CASH -import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.`issued by` -import net.corda.contracts.asset.`owned by` import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.* import net.corda.core.crypto.* @@ -33,8 +28,15 @@ import net.corda.core.utilities.days import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.toNonEmptySet import net.corda.core.utilities.unwrap -import net.corda.flows.TwoPartyTradeFlow.Buyer -import net.corda.flows.TwoPartyTradeFlow.Seller +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.contracts.asset.CASH +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.`issued by` +import net.corda.finance.contracts.asset.`owned by` +import net.corda.finance.flows.TwoPartyTradeFlow.Buyer +import net.corda.finance.flows.TwoPartyTradeFlow.Seller import net.corda.node.internal.AbstractNode import net.corda.node.services.api.WritableTransactionStorage import net.corda.node.services.config.NodeConfiguration @@ -323,6 +325,11 @@ class TwoPartyTradeFlowTests { val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) + allNodes.forEach { node -> + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.verifyAndRegisterIdentity(identity) } + } + ledger(aliceNode.services, initialiseSerialization = false) { // Insert a prospectus type attachment into the commercial paper transaction. @@ -424,6 +431,11 @@ class TwoPartyTradeFlowTests { val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name) val issuer = bankNode.info.legalIdentity.ref(1, 2, 3) + val allNodes = listOf(notaryNode, aliceNode, bobNode, bankNode) + allNodes.forEach { node -> + allNodes.map { it.services.myInfo.legalIdentityAndCert }.forEach { identity -> node.services.identityService.verifyAndRegisterIdentity(identity) } + } + ledger(aliceNode.services, initialiseSerialization = false) { // Insert a prospectus type attachment into the commercial paper transaction. diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index fd5fff4e00..3ffcee67e6 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -15,6 +15,7 @@ import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.getTestPartyAndCertificate import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThatExceptionOfType @@ -168,7 +169,7 @@ fun issueState(node: AbstractNode, notaryNode: AbstractNode): StateAndRef<*> { fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: AbstractNode): StateAndRef { val state = TransactionState(DummyContract.MultiOwnerState(0, listOf(nodeA.info.legalIdentity, nodeB.info.legalIdentity)), notaryNode.info.notaryIdentity) - val tx = TransactionBuilder(notary = notaryNode.info.notaryIdentity).withItems(state) + val tx = TransactionBuilder(notary = notaryNode.info.notaryIdentity).withItems(state, dummyCommand()) val signedByA = nodeA.services.signInitialTransaction(tx) val signedByAB = nodeB.services.addSignature(signedByA) val stx = notaryNode.services.addSignature(signedByAB, notaryNode.services.notaryIdentityKey) diff --git a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt index 923457f44b..6cbad996e3 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/HibernateConfigurationTest.kt @@ -1,7 +1,9 @@ package net.corda.node.services.database -import net.corda.contracts.asset.* -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef +import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toBase58String import net.corda.core.node.services.Vault @@ -10,14 +12,22 @@ import net.corda.core.schemas.CommonSchemaV1 import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction +import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS +import net.corda.finance.SWISS_FRANCS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY +import net.corda.finance.contracts.asset.DummyFungibleContract +import net.corda.finance.schemas.CashSchemaV1 +import net.corda.finance.schemas.SampleCashSchemaV2 +import net.corda.finance.schemas.SampleCashSchemaV3 +import net.corda.finance.utils.sumCash import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase -import net.corda.schemas.CashSchemaV1 -import net.corda.schemas.SampleCashSchemaV2 -import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.* import net.corda.testing.contracts.consumeCash import net.corda.testing.contracts.fillWithSomeTestCash @@ -65,14 +75,15 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { issuerServices = MockServices(DUMMY_CASH_ISSUER_KEY, BOB_KEY, BOC_KEY) val dataSourceProps = makeTestDataSourceProperties() val defaultDatabaseProperties = makeTestDatabaseProperties() - database = configureDatabase(dataSourceProps, defaultDatabaseProperties, identitySvc = ::makeTestIdentityService) val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3) + val createSchemaService = { NodeSchemaService(customSchemas) } + database = configureDatabase(dataSourceProps, defaultDatabaseProperties, createSchemaService, ::makeTestIdentityService) database.transaction { - hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties(), ::makeTestIdentityService) + hibernateConfig = database.hibernateConfig services = object : MockServices(BOB_KEY, BOC_KEY, DUMMY_NOTARY_KEY) { - override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig) + override val vaultService: VaultService = makeVaultService(database.hibernateConfig) - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) } @@ -638,9 +649,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // search predicate val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) - val joinCashToParty = cashStatesSchema.join("owner") - val queryOwnerKey = BOB_PUBKEY.toBase58String() - criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryOwnerKey)) + val queryOwner = BOB.name.toString() + criteriaQuery.where(criteriaBuilder.equal(cashStatesSchema.get("owner"), queryOwner)) val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get("stateRef"), cashStatesSchema.get("stateRef")) criteriaQuery.where(joinVaultStatesToCash) @@ -723,9 +733,9 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() { // search predicate val cashStatesSchema = criteriaQuery.from(SampleCashSchemaV3.PersistentCashState::class.java) - val joinCashToParty = cashStatesSchema.join("participants") - val queryParticipantKeys = firstCashState.state.data.participants.map { it.owningKey.toBase58String() } - criteriaQuery.where(criteriaBuilder.equal(joinCashToParty.get("key"), queryParticipantKeys)) + val queryParticipants = firstCashState.state.data.participants.map { it.nameOrNull().toString() } + val joinCashStateToParty = cashStatesSchema.joinSet("participants") + criteriaQuery.where(criteriaBuilder.and(joinCashStateToParty.`in`(queryParticipants))) val joinVaultStatesToCash = criteriaBuilder.equal(vaultStates.get("stateRef"), cashStatesSchema.get("stateRef")) criteriaQuery.where(joinVaultStatesToCash) diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index db05d880db..e69de29bb2 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -1,213 +0,0 @@ -package net.corda.node.services.database - -import io.requery.Persistable -import io.requery.kotlin.eq -import io.requery.sql.KotlinEntityDataStore -import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* -import net.corda.core.identity.AnonymousParty -import net.corda.core.node.services.Vault -import net.corda.core.serialization.serialize -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.WireTransaction -import net.corda.node.services.persistence.DBTransactionStorage -import net.corda.node.services.vault.schemas.requery.Models -import net.corda.node.services.vault.schemas.requery.VaultCashBalancesEntity -import net.corda.node.services.vault.schemas.requery.VaultSchema -import net.corda.node.services.vault.schemas.requery.VaultStatesEntity -import net.corda.node.utilities.CordaPersistence -import net.corda.node.utilities.configureDatabase -import net.corda.testing.* -import net.corda.testing.contracts.DummyContract -import net.corda.testing.node.makeTestDataSourceProperties -import net.corda.testing.node.makeTestDatabaseProperties -import net.corda.testing.node.makeTestIdentityService -import org.assertj.core.api.Assertions -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import java.time.Instant -import java.util.* - -class RequeryConfigurationTest : TestDependencyInjectionBase() { - - lateinit var database: CordaPersistence - lateinit var transactionStorage: DBTransactionStorage - lateinit var requerySession: KotlinEntityDataStore - - @Before - fun setUp() { - val dataSourceProperties = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) - newTransactionStorage() - newRequeryStorage(dataSourceProperties) - } - - @After - fun cleanUp() { - database.close() - } - - @Test - fun `transaction inserts in same DB transaction scope across two persistence engines`() { - val txn = newTransaction() - - database.transaction { - transactionStorage.addTransaction(txn) - requerySession.withTransaction { - insert(createVaultStateEntity(txn)) - } - } - - database.transaction { - Assertions.assertThat(transactionStorage.transactions).containsOnly(txn) - requerySession.withTransaction { - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString()) - Assertions.assertThat(result.get().first().txId).isEqualTo(txn.tx.inputs[0].txhash.toString()) - } - } - } - - @Test - fun `transaction operations in same DB transaction scope across two persistence engines`() { - val txn = newTransaction() - - database.transaction { - transactionStorage.addTransaction(txn) - requerySession.withTransaction { - upsert(createCashBalance()) - select(VaultSchema.VaultCashBalances::class).get().first() - insert(createVaultStateEntity(txn)) - } - } - - database.transaction { - Assertions.assertThat(transactionStorage.transactions).containsOnly(txn) - requerySession.withTransaction { - val cashQuery = select(VaultSchema.VaultCashBalances::class) where (VaultSchema.VaultCashBalances::currency eq "GBP") - assertEquals(12345, cashQuery.get().first().amount) - val stateQuery = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString()) - Assertions.assertThat(stateQuery.get().first().txId).isEqualTo(txn.tx.inputs[0].txhash.toString()) - } - } - } - - @Test - fun `transaction rollback in same DB transaction scope across two persistence engines`() { - val txn = newTransaction() - - database.transaction { - transactionStorage.addTransaction(txn) - requerySession.withTransaction { - insert(createVaultStateEntity(txn)) - } - rollback() - } - - database.transaction { - Assertions.assertThat(transactionStorage.transactions).isEmpty() - requerySession.withTransaction { - val result = select(VaultSchema.VaultStates::class) where (VaultSchema.VaultStates::txId eq txn.tx.inputs[0].txhash.toString()) - Assertions.assertThat(result.get().count() == 0) - } - } - } - - @Test - fun `bounded iteration`() { - // insert 100 entities - database.transaction { - requerySession.withTransaction { - (1..100) - .map { newTransaction(it) } - .forEach { insert(createVaultStateEntity(it)) } - } - } - - // query entities 41..45 - database.transaction { - requerySession.withTransaction { - // Note: cannot specify a limit explicitly when using iterator skip & take - val query = select(VaultSchema.VaultStates::class) - val count = query.get().count() - Assertions.assertThat(count).isEqualTo(100) - val result = query.get().iterator(40, 5) - Assertions.assertThat(result.asSequence().count()).isEqualTo(5) - } - } - } - - @Test - fun `test calling an arbitrary JDBC native query`() { - val txn = newTransaction() - - database.transaction { - transactionStorage.addTransaction(txn) - requerySession.withTransaction { - insert(createVaultStateEntity(txn)) - } - } - - val dataSourceProperties = makeTestDataSourceProperties() - val nativeQuery = "SELECT v.transaction_id, v.output_index FROM vault_states v WHERE v.state_status = 0" - - database.transaction { - val configuration = RequeryConfiguration(dataSourceProperties, true, makeTestDatabaseProperties()) - val jdbcSession = configuration.jdbcSession() - val prepStatement = jdbcSession.prepareStatement(nativeQuery) - val rs = prepStatement.executeQuery() - assertTrue(rs.next()) - assertEquals(rs.getString(1), txn.tx.inputs[0].txhash.toString()) - assertEquals(rs.getInt(2), txn.tx.inputs[0].index) - } - } - - private fun createVaultStateEntity(txn: SignedTransaction): VaultStatesEntity { - val txnState = txn.tx.inputs[0] - val state = VaultStatesEntity().apply { - txId = txnState.txhash.toString() - index = txnState.index - stateStatus = Vault.StateStatus.UNCONSUMED - contractStateClassName = DummyContract.SingleOwnerState::class.java.name - contractState = DummyContract.SingleOwnerState(owner = AnonymousParty(MEGA_CORP_PUBKEY)).serialize().bytes - notaryName = txn.tx.notary!!.name.toString() - notaryKey = txn.tx.notary!!.owningKey.toBase58String() - recordedTime = Instant.now() - } - return state - } - - private fun createCashBalance(): VaultCashBalancesEntity { - val cashBalanceEntity = VaultCashBalancesEntity() - cashBalanceEntity.currency = "GBP" - cashBalanceEntity.amount = 12345 - return cashBalanceEntity - } - - private fun newTransactionStorage() { - database.transaction { - transactionStorage = DBTransactionStorage() - } - } - - private fun newRequeryStorage(dataSourceProperties: Properties) { - database.transaction { - val configuration = RequeryConfiguration(dataSourceProperties, true, makeTestDatabaseProperties()) - requerySession = configuration.sessionForModel(Models.VAULT) - } - } - - private fun newTransaction(index: Int = 0): SignedTransaction { - val wtx = WireTransaction( - inputs = listOf(StateRef(SecureHash.randomSHA256(), index)), - attachments = emptyList(), - outputs = emptyList(), - commands = emptyList(), - notary = DUMMY_NOTARY, - timeWindow = null - ) - return SignedTransaction(wtx, listOf(TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID)))) - } -} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index 2480ad4cab..df17f414c2 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -11,8 +11,10 @@ import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.TransactionBuilder import net.corda.node.services.MockServiceHubInternal +import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage +import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.vault.NodeVaultService @@ -71,7 +73,8 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { smmHasRemovedAllFlows = CountDownLatch(1) calls = 0 val dataSourceProps = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + val databaseProperties = makeTestDatabaseProperties() + database = configureDatabase(dataSourceProps, databaseProperties, createIdentityService = ::makeTestIdentityService) val identityService = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) val kms = MockKeyManagementService(identityService, ALICE_KEY) @@ -88,7 +91,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { - override val vaultService: VaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties()) + override val vaultService: VaultService = NodeVaultService(this) override val testReference = this@NodeSchedulerServiceTest } smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index 97ff1b7182..b57af97ed6 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -22,6 +22,7 @@ import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Assert.assertTrue @@ -71,7 +72,8 @@ class ScheduledFlowTests { val notary = serviceHub.networkMapCache.getAnyNotary() val builder = TransactionBuilder(notary) - builder.withItems(scheduledState) + .addOutputState(scheduledState) + .addCommand(dummyCommand(serviceHub.legalIdentityKey)) val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx, setOf(serviceHub.myInfo.legalIdentity))) } @@ -91,7 +93,9 @@ class ScheduledFlowTests { val notary = state.state.notary val newStateOutput = scheduledState.copy(processed = true) val builder = TransactionBuilder(notary) - builder.withItems(state, newStateOutput) + .addInputState(state) + .addOutputState(newStateOutput) + .addCommand(dummyCommand(serviceHub.legalIdentityKey)) val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx, setOf(scheduledState.source, scheduledState.destination))) } diff --git a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt index 0d19c27a65..098b7c41de 100644 --- a/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/messaging/ArtemisMessagingTests.kt @@ -9,11 +9,9 @@ import net.corda.core.messaging.RPCOps import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl -import net.corda.node.services.api.DEFAULT_SESSION_ID import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate -import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.PersistentUniquenessProvider @@ -71,7 +69,7 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { baseDirectory = baseDirectory, myLegalName = ALICE.name) LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) networkMapRegistrationFuture = doneFuture(Unit) } @@ -126,7 +124,7 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { val receivedMessages = LinkedBlockingQueue() val messagingClient = createAndStartClientAndServer(receivedMessages) - val message = messagingClient.createMessage(topic, DEFAULT_SESSION_ID, "first msg".toByteArray()) + val message = messagingClient.createMessage(topic, data = "first msg".toByteArray()) messagingClient.send(message, messagingClient.myAddress) val actual: Message = receivedMessages.take() @@ -142,10 +140,10 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { val receivedMessages = LinkedBlockingQueue() val messagingClient = createAndStartClientAndServer(receivedMessages) - val message = messagingClient.createMessage(topic, DEFAULT_SESSION_ID, "first msg".toByteArray()) + val message = messagingClient.createMessage(topic, data = "first msg".toByteArray()) messagingClient.send(message, messagingClient.myAddress) - val networkMapMessage = messagingClient.createMessage(NetworkMapService.FETCH_TOPIC, DEFAULT_SESSION_ID, "second msg".toByteArray()) + val networkMapMessage = messagingClient.createMessage(NetworkMapService.FETCH_TOPIC, data = "second msg".toByteArray()) messagingClient.send(networkMapMessage, messagingClient.myAddress) val actual: Message = receivedMessages.take() @@ -167,11 +165,11 @@ class ArtemisMessagingTests : TestDependencyInjectionBase() { val messagingClient = createAndStartClientAndServer(receivedMessages) for (iter in 1..iterations) { - val message = messagingClient.createMessage(topic, DEFAULT_SESSION_ID, "first msg $iter".toByteArray()) + val message = messagingClient.createMessage(topic, data = "first msg $iter".toByteArray()) messagingClient.send(message, messagingClient.myAddress) } - val networkMapMessage = messagingClient.createMessage(NetworkMapService.FETCH_TOPIC, DEFAULT_SESSION_ID, "second msg".toByteArray()) + val networkMapMessage = messagingClient.createMessage(NetworkMapService.FETCH_TOPIC, data = "second msg".toByteArray()) messagingClient.send(networkMapMessage, messagingClient.myAddress) val actual: Message = receivedMessages.take() diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 9d385513fb..c817fca191 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -6,8 +6,8 @@ import net.corda.core.node.NodeInfo import net.corda.core.node.services.ServiceInfo import net.corda.core.serialization.deserialize import net.corda.core.utilities.getOrThrow -import net.corda.node.services.api.DEFAULT_SESSION_ID import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.send import net.corda.node.services.messaging.sendRequest import net.corda.node.services.network.AbstractNetworkMapServiceTest.Changed.Added @@ -228,7 +228,7 @@ abstract class AbstractNetworkMapServiceTest private fun MockNode.subscribe(): Queue { val request = SubscribeRequest(true, network.myAddress) val updates = LinkedBlockingQueue() - services.networkService.addMessageHandler(PUSH_TOPIC, DEFAULT_SESSION_ID) { message, _ -> + services.networkService.addMessageHandler(PUSH_TOPIC) { message, _ -> updates += message.data.deserialize() } val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.network.myAddress) @@ -246,7 +246,7 @@ abstract class AbstractNetworkMapServiceTest private fun MockNode.ackUpdate(mapVersion: Int) { val request = UpdateAcknowledge(mapVersion, services.networkService.myAddress) - services.networkService.send(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, request, mapServiceNode.network.myAddress) + services.networkService.send(PUSH_ACK_TOPIC, MessagingService.DEFAULT_SESSION_ID, request, mapServiceNode.network.myAddress) mockNet.runNetwork() } diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index 4507bd395a..c7809739fb 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -7,7 +7,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate -import net.corda.core.node.services.IdentityService +import net.corda.core.node.services.UnknownAnonymousPartyException import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.CertificateType import net.corda.node.utilities.X509Utilities @@ -90,10 +90,10 @@ class InMemoryIdentityServiceTests { val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) // TODO: Generate certificate with an EdDSA key rather than ECDSA - val identity = Party(CertificateAndKeyPair(rootCert, rootKey)) + val identity = Party(rootCert) val txIdentity = AnonymousParty(txKey.public) - assertFailsWith { + assertFailsWith { service.assertOwnership(identity, txIdentity) } } @@ -107,7 +107,7 @@ class InMemoryIdentityServiceTests { fun `get anonymous identity by key`() { val trustRoot = DUMMY_CA val (alice, aliceTxIdentity) = createParty(ALICE.name, trustRoot) - val (bob, bobTxIdentity) = createParty(ALICE.name, trustRoot) + val (_, bobTxIdentity) = createParty(ALICE.name, trustRoot) // Now we have identities, construct the service and let it know about both val service = InMemoryIdentityService(setOf(alice), emptySet(), trustRoot.certificate.cert) @@ -163,7 +163,7 @@ class InMemoryIdentityServiceTests { val txKey = Crypto.generateKeyPair() val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public) val txCertPath = certFactory.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) - return Pair(issuer, PartyAndCertificate(Party(x500Name, txKey.public), txCert, txCertPath)) + return Pair(issuer, PartyAndCertificate(txCertPath)) } /** diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt index e060680d41..88b0012435 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBCheckpointStorageTests.kt @@ -33,7 +33,7 @@ class DBCheckpointStorageTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) newCheckpointStorage() } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index ba59ceefa1..063dbff380 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -4,12 +4,15 @@ import net.corda.core.contracts.StateRef import net.corda.core.crypto.Crypto import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignatureMetadata -import net.corda.core.node.services.VaultService import net.corda.core.crypto.TransactionSignature +import net.corda.core.node.services.VaultService import net.corda.core.schemas.MappedSchema import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction +import net.corda.finance.schemas.CashSchemaV1 +import net.corda.finance.schemas.SampleCashSchemaV2 +import net.corda.finance.schemas.SampleCashSchemaV3 import net.corda.node.services.database.HibernateConfiguration import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService @@ -18,9 +21,6 @@ import net.corda.node.services.vault.NodeVaultService import net.corda.node.services.vault.VaultSchemaV1 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase -import net.corda.schemas.CashSchemaV1 -import net.corda.schemas.SampleCashSchemaV2 -import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.* import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties @@ -38,8 +38,6 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { lateinit var transactionStorage: DBTransactionStorage lateinit var services: MockServices val vault: VaultService get() = services.vaultService - // Hibernate configuration objects - lateinit var hibernateConfig: HibernateConfiguration @Before fun setUp() { @@ -49,18 +47,16 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { val transactionSchema = MappedSchema(schemaFamily = javaClass, version = 1, mappedTypes = listOf(DBTransactionStorage.DBTransaction::class.java)) - val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3, transactionSchema) + val createSchemaService = { NodeSchemaService(setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3, transactionSchema)) } - database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), customSchemas, identitySvc = ::makeTestIdentityService) + database = configureDatabase(dataSourceProps, makeTestDatabaseProperties(), createSchemaService, ::makeTestIdentityService) database.transaction { - hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) - services = object : MockServices(BOB_KEY) { override val vaultService: VaultService get() { - val vaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties()) - hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig) + val vaultService = NodeVaultService(this) + hibernatePersister = HibernateObserver(vaultService.rawUpdates, database.hibernateConfig) return vaultService } @@ -220,7 +216,7 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() { inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)), attachments = emptyList(), outputs = emptyList(), - commands = emptyList(), + commands = listOf(dummyCommand()), notary = DUMMY_NOTARY, timeWindow = null ) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt index 3f1f0e909c..d9b46706bd 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt @@ -1,10 +1,8 @@ package net.corda.node.services.persistence import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.Issued -import net.corda.core.contracts.USD import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow @@ -13,6 +11,8 @@ import net.corda.core.identity.Party import net.corda.core.node.services.queryBy import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.USD +import net.corda.finance.contracts.asset.Cash import net.corda.node.services.NotifyTransactionHandler import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index 287c4638cb..f1c55ca1a7 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -7,14 +7,13 @@ import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.internal.read import net.corda.core.internal.readAll -import net.corda.testing.LogHelper import net.corda.core.internal.write import net.corda.core.internal.writeLines -import net.corda.node.services.database.RequeryConfiguration -import net.corda.node.services.persistence.schemas.requery.AttachmentEntity import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.configureDatabase +import net.corda.testing.LogHelper import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseProperties import net.corda.testing.node.makeTestIdentityService @@ -25,7 +24,6 @@ import java.nio.charset.Charset import java.nio.file.FileAlreadyExistsException import java.nio.file.FileSystem import java.nio.file.Path -import java.util.* import java.util.jar.JarEntry import java.util.jar.JarOutputStream import kotlin.test.assertEquals @@ -36,17 +34,13 @@ class NodeAttachmentStorageTest { // Use an in memory file system for testing attachment storage. lateinit var fs: FileSystem lateinit var database: CordaPersistence - lateinit var dataSourceProperties: Properties - lateinit var configuration: RequeryConfiguration @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - dataSourceProperties = makeTestDataSourceProperties() - database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) - - configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties()) + val dataSourceProperties = makeTestDataSourceProperties() + database = configureDatabase(dataSourceProperties, makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) fs = Jimfs.newFileSystem(Configuration.unix()) } @@ -61,7 +55,7 @@ class NodeAttachmentStorageTest { val expectedHash = testJar.readAll().sha256() database.transaction { - val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties()) + val storage = NodeAttachmentService(MetricRegistry()) val id = testJar.read { storage.importAttachment(it) } assertEquals(expectedHash, id) @@ -87,7 +81,7 @@ class NodeAttachmentStorageTest { fun `duplicates not allowed`() { val testJar = makeTestJar() database.transaction { - val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties()) + val storage = NodeAttachmentService(MetricRegistry()) testJar.read { storage.importAttachment(it) } @@ -102,19 +96,21 @@ class NodeAttachmentStorageTest { @Test fun `corrupt entry throws exception`() { val testJar = makeTestJar() + val id = database.transaction { - val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties()) + val storage = NodeAttachmentService(MetricRegistry()) val id = testJar.read { storage.importAttachment(it) } // Corrupt the file in the store. val bytes = testJar.readAll() val corruptBytes = "arggghhhh".toByteArray() System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) - val corruptAttachment = AttachmentEntity() - corruptAttachment.attId = id - corruptAttachment.content = bytes - storage.session.update(corruptAttachment) - + val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) + DatabaseTransactionManager.current().session.merge(corruptAttachment) + id + } + database.transaction { + val storage = NodeAttachmentService(MetricRegistry()) val e = assertFailsWith { storage.openAttachment(id)!!.open().use { it.readBytes() } } @@ -131,7 +127,7 @@ class NodeAttachmentStorageTest { @Test fun `non jar rejected`() { database.transaction { - val storage = NodeAttachmentService(dataSourceProperties, MetricRegistry(), makeTestDatabaseProperties()) + val storage = NodeAttachmentService(MetricRegistry()) val path = fs.getPath("notajar") path.writeLines(listOf("Hey", "there!")) path.read { diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index f81aac2b48..66c3d5dad0 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -9,19 +9,14 @@ import net.corda.core.schemas.PersistentState import net.corda.core.schemas.QueryableState import net.corda.testing.LogHelper import net.corda.node.services.api.SchemaService -import net.corda.node.services.database.HibernateConfiguration -import net.corda.node.services.identity.InMemoryIdentityService -import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.configureDatabase -import net.corda.testing.DUMMY_CA import net.corda.testing.MEGA_CORP -import net.corda.testing.MOCK_IDENTITIES import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseProperties import net.corda.testing.node.makeTestIdentityService import org.hibernate.annotations.Cascade import org.hibernate.annotations.CascadeType -import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.After import org.junit.Before import org.junit.Test @@ -31,17 +26,14 @@ import kotlin.test.assertEquals class HibernateObserverTests { - lateinit var database: CordaPersistence @Before fun setUp() { LogHelper.setLevel(HibernateObserver::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) } @After fun cleanUp() { - database.close() LogHelper.reset(HibernateObserver::class) } @@ -104,21 +96,24 @@ class HibernateObserverTests { return parent } } + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), { schemaService }, createIdentityService = ::makeTestIdentityService) @Suppress("UNUSED_VARIABLE") - val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService, makeTestDatabaseProperties(), ::makeTestIdentityService)) + val observer = HibernateObserver(rawUpdatesPublisher, database.hibernateConfig) database.transaction { rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0))))) - val parentRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() + val parentRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery() parentRowCountResult.next() val parentRows = parentRowCountResult.getInt(1) parentRowCountResult.close() - val childrenRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery() + val childrenRowCountResult = DatabaseTransactionManager.current().connection.prepareStatement("select count(*) from Children").executeQuery() childrenRowCountResult.next() val childrenRows = childrenRowCountResult.getInt(1) childrenRowCountResult.close() assertEquals(1, parentRows, "Expected one parent") assertEquals(2, childrenRows, "Expected two children") } + + database.close() } } \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 41118d6f83..64d21ded83 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -5,7 +5,6 @@ import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.concurrent.Semaphore import net.corda.core.concurrent.CordaFuture import net.corda.core.contracts.ContractState -import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.random63BitValue @@ -27,8 +26,9 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Change import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueFlow +import net.corda.finance.flows.CashPaymentFlow import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.checkpoints @@ -331,9 +331,7 @@ class FlowFrameworkTests { node1.services.startFlow(CashIssueFlow( 2000.DOLLARS, OpaqueBytes.of(0x01), - node1.info.legalIdentity, - notary1.info.notaryIdentity, - anonymous = false)) + notary1.info.notaryIdentity)) // We pay a couple of times, the notary picking should go round robin for (i in 1..3) { val flow = node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity, anonymous = false)) @@ -595,7 +593,8 @@ class FlowFrameworkTests { @Test fun `wait for transaction`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) - ptx.addOutputState(DummyState()) + .addOutputState(DummyState()) + .addCommand(dummyCommand(node1.services.legalIdentityKey)) val stx = node1.services.signInitialTransaction(ptx) val committerFiber = node1.registerFlowFactory(WaitingFlows.Waiter::class) { @@ -609,7 +608,8 @@ class FlowFrameworkTests { @Test fun `committer throws exception before calling the finality flow`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) - ptx.addOutputState(DummyState()) + .addOutputState(DummyState()) + .addCommand(dummyCommand()) val stx = node1.services.signInitialTransaction(ptx) node1.registerFlowFactory(WaitingFlows.Waiter::class) { @@ -625,7 +625,8 @@ class FlowFrameworkTests { @Test fun `verify vault query service is tokenizable by force checkpointing within a flow`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) - ptx.addOutputState(DummyState()) + .addOutputState(DummyState()) + .addCommand(dummyCommand(node1.services.legalIdentityKey)) val stx = node1.services.signInitialTransaction(ptx) node1.registerFlowFactory(VaultQueryFlow::class) { diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt index 95082a625b..f06897bae5 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/DistributedImmutableMapTests.kt @@ -10,12 +10,12 @@ import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.node.services.network.NetworkMapService import net.corda.node.utilities.CordaPersistence +import net.corda.node.utilities.DatabaseTransaction import net.corda.node.utilities.configureDatabase import net.corda.testing.* import net.corda.testing.node.makeTestDataSourceProperties import net.corda.testing.node.makeTestDatabaseProperties import net.corda.testing.node.makeTestIdentityService -import org.jetbrains.exposed.sql.Transaction import org.junit.After import org.junit.Before import org.junit.Test @@ -27,14 +27,13 @@ class DistributedImmutableMapTests : TestDependencyInjectionBase() { data class Member(val client: CopycatClient, val server: CopycatServer) lateinit var cluster: List - lateinit var transaction: Transaction - lateinit var database: CordaPersistence + lateinit var transaction: DatabaseTransaction + private val databases: MutableList = mutableListOf() @Before fun setup() { LogHelper.setLevel("-org.apache.activemq") LogHelper.setLevel(NetworkMapService::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) cluster = setUpCluster() } @@ -46,7 +45,7 @@ class DistributedImmutableMapTests : TestDependencyInjectionBase() { it.client.close() it.server.shutdown() } - database.close() + databases.forEach { it.close() } } @Test @@ -87,8 +86,9 @@ class DistributedImmutableMapTests : TestDependencyInjectionBase() { private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture { val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build() val address = Address(myAddress.host, myAddress.port) - - val stateMachineFactory = { DistributedImmutableMap(database, "commited_states_${myAddress.port}") } + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties("serverNameTablePrefix", "PORT_${myAddress.port}_"), createIdentityService = ::makeTestIdentityService) + databases.add(database) + val stateMachineFactory = { DistributedImmutableMap(database, RaftUniquenessProvider.Companion::createMap) } val server = CopycatServer.builder(address) .withStateMachine(stateMachineFactory) diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index e72e273dc1..4eb85f9f01 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -16,6 +16,7 @@ import net.corda.node.internal.AbstractNode import net.corda.node.services.network.NetworkMapService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -50,8 +51,10 @@ class NotaryServiceTests { fun `should sign a unique transaction with a valid time-window`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.setTimeWindow(Instant.now(), 30.seconds) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) + .setTimeWindow(Instant.now(), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -64,7 +67,9 @@ class NotaryServiceTests { fun `should sign a unique transaction without a time-window`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } @@ -77,8 +82,10 @@ class NotaryServiceTests { fun `should report error for transaction with an invalid time-window`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) + .setTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -92,7 +99,9 @@ class NotaryServiceTests { fun `should sign identical transaction multiple times (signing is idempotent)`() { val stx = run { val inputState = issueState(clientNode) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } @@ -110,12 +119,16 @@ class NotaryServiceTests { fun `should report conflict when inputs are reused across transactions`() { val inputState = issueState(clientNode) val stx = run { - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } val stx2 = run { - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.addInputState(issueState(clientNode)) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addInputState(issueState(clientNode)) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt index 57eefb0f22..289bc695ac 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PersistentUniquenessProviderTests.kt @@ -23,7 +23,7 @@ class PersistentUniquenessProviderTests : TestDependencyInjectionBase() { @Before fun setUp() { LogHelper.setLevel(PersistentUniquenessProvider::class) - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) } @After diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 335ec1048b..dadecbc5f7 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -18,6 +18,7 @@ import net.corda.node.services.network.NetworkMapService import net.corda.testing.DUMMY_NOTARY import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.contracts.DummyContract +import net.corda.testing.dummyCommand import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.After @@ -52,7 +53,9 @@ class ValidatingNotaryServiceTests { fun `should report error for invalid transaction dependency`() { val stx = run { val inputState = issueInvalidState(clientNode, notaryNode.info.notaryIdentity) - val tx = TransactionBuilder(notaryNode.info.notaryIdentity).withItems(inputState) + val tx = TransactionBuilder(notaryNode.info.notaryIdentity) + .addInputState(inputState) + .addCommand(dummyCommand(clientNode.services.legalIdentityKey)) clientNode.services.signInitialTransaction(tx) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 418c62f631..3d481851ae 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -1,12 +1,10 @@ package net.corda.node.services.vault import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.contracts.asset.sumCash -import net.corda.contracts.getCashBalance -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Issued +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.StateRef import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -20,6 +18,12 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.NonEmptySet import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.toNonEmptySet +import net.corda.finance.* +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY +import net.corda.finance.contracts.getCashBalance +import net.corda.finance.utils.sumCash import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.fillWithSomeTestCash @@ -68,11 +72,11 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { lockId: UUID = UUID.randomUUID(), withIssuerRefs: Set? = null): List> { - val notaryName = if (notary != null) listOf(notary.name) else null - var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = notaryName) + val notaries = if (notary != null) listOf(notary) else null + var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = notaries) if (onlyFromIssuerParties != null || withIssuerRefs != null) { baseCriteria = baseCriteria.and(QueryCriteria.FungibleAssetQueryCriteria( - issuerPartyName = onlyFromIssuerParties?.toList(), + issuer = onlyFromIssuerParties?.toList(), issuerRef = withIssuerRefs?.toList())) } @@ -83,9 +87,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `states not local to instance`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L)) - + } + database.transaction { val w1 = vaultQuery.queryBy().states assertThat(w1).hasSize(3) @@ -93,13 +97,14 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { val originalVaultQuery = vaultQuery val services2 = object : MockServices() { override val vaultService: VaultService get() = originalVault - override fun recordTransactions(txs: Iterable) { + override fun recordTransactions(notifyVault: Boolean, txs: Iterable) { for (stx in txs) { validatedTransactions.addTransaction(stx) vaultService.notify(stx.tx) } } - override val vaultQueryService : VaultQueryService get() = originalVaultQuery + + override val vaultQueryService: VaultQueryService get() = originalVaultQuery } val w2 = services2.vaultQueryService.queryBy().states @@ -110,9 +115,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `states for refs`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L)) - + } + database.transaction { val w1 = vaultQuery.queryBy().states assertThat(w1).hasSize(3) @@ -124,8 +129,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `states soft locking reserve and release`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L)) + } + database.transaction { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(3) @@ -307,8 +313,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `unconsumedStatesForSpending exact amount`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L)) + } + database.transaction { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(1) @@ -325,10 +332,10 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `unconsumedStatesForSpending from two issuer parties`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1))) - + } + database.transaction { val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS, onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)) spendableStatesUSD.forEach(::println) @@ -342,12 +349,12 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `unconsumedStatesForSpending from specific issuer party and refs`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), ref = OpaqueBytes.of(1)) services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), ref = OpaqueBytes.of(2)) services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), ref = OpaqueBytes.of(3)) - + } + database.transaction { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(4) @@ -364,9 +371,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `unconsumedStatesForSpending insufficient amount`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(1) @@ -381,9 +388,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `unconsumedStatesForSpending small amount`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L)) - + } + database.transaction { val unconsumedStates = vaultQuery.queryBy().states assertThat(unconsumedStates).hasSize(2) @@ -399,11 +406,11 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() { @Test fun `states soft locking query granularity`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L)) - + } + database.transaction { var unlockedStates = 30 val allStates = vaultQuery.queryBy().states assertThat(allStates).hasSize(unlockedStates) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 47ece34c00..707361794c 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -1,26 +1,28 @@ package net.corda.node.services.vault -import net.corda.contracts.CommercialPaper -import net.corda.contracts.Commodity -import net.corda.contracts.DealState -import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.entropyToKeyPair import net.corda.core.crypto.toBase58String import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.* import net.corda.core.node.services.vault.* import net.corda.core.node.services.vault.QueryCriteria.* import net.corda.core.utilities.* +import net.corda.finance.* +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.contracts.Commodity +import net.corda.finance.contracts.DealState +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY +import net.corda.finance.schemas.CashSchemaV1 +import net.corda.finance.schemas.CashSchemaV1.PersistentCashState +import net.corda.finance.schemas.CommercialPaperSchemaV1 +import net.corda.finance.schemas.SampleCashSchemaV3 import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase -import net.corda.schemas.CashSchemaV1 -import net.corda.schemas.CashSchemaV1.PersistentCashState -import net.corda.schemas.CommercialPaperSchemaV1 -import net.corda.schemas.SampleCashSchemaV3 import net.corda.testing.* import net.corda.testing.contracts.* import net.corda.testing.node.MockServices @@ -49,11 +51,20 @@ class VaultQueryTests : TestDependencyInjectionBase() { lateinit var notaryServices: MockServices val vaultSvc: VaultService get() = services.vaultService val vaultQuerySvc: VaultQueryService get() = services.vaultQueryService + val identitySvc: IdentityService = makeTestIdentityService() lateinit var database: CordaPersistence + // test cash notary + val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) } + val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) + val CASH_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(CASH_NOTARY.nameOrNull(), CASH_NOTARY_KEY.public) + @Before fun setUp() { - val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY)) + // register additional identities + identitySvc.verifyAndRegisterIdentity(CASH_NOTARY_IDENTITY) + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) + val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY, DUMMY_NOTARY_KEY), createIdentityService = { identitySvc }) database = databaseAndServices.first services = databaseAndServices.second notaryServices = MockServices(DUMMY_NOTARY_KEY, DUMMY_CASH_ISSUER_KEY, BOC_KEY, MEGA_CORP_KEY) @@ -70,7 +81,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Ignore @Test fun createPersistentTestDb() { - val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + val database = configureDatabase(makePersistentDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = { identitySvc }) setUpDb(database, 5000) @@ -125,11 +136,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states simple`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { // DOCSTART VaultQueryExample1 val result = vaultQuerySvc.queryBy() @@ -152,11 +163,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states verbose`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { val criteria = VaultQueryCriteria() // default is UNCONSUMED val result = vaultQuerySvc.queryBy(criteria) @@ -168,20 +179,22 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states with count`() { database.transaction { - services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(4) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) - + } + database.transaction { services.consumeCash(75.DOLLARS, notary = DUMMY_NOTARY) - + } + database.transaction { val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) assertThat(resultsAfterConsume.states).hasSize(1) @@ -192,11 +205,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash states simple`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { val result = vaultQuerySvc.queryBy() assertThat(result.states).hasSize(3) @@ -207,11 +220,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash states verbose`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { val criteria = VaultQueryCriteria() // default is UNCONSUMED val result = vaultQuerySvc.queryBy(criteria) @@ -222,19 +235,19 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash states sorted by state ref`() { + val stateRefs: MutableList = mutableListOf() database.transaction { - - val stateRefs: MutableList = mutableListOf() - val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 10, 10, Random(0L)) val issuedStateRefs = issuedStates.states.map { it.ref }.toList() stateRefs.addAll(issuedStateRefs) - + } + database.transaction { val spentStates = services.consumeCash(25.DOLLARS, notary = DUMMY_NOTARY) val consumedStateRefs = spentStates.consumed.map { it.ref }.toList() val producedStateRefs = spentStates.produced.map { it.ref }.toList() stateRefs.addAll(consumedStateRefs.plus(producedStateRefs)) - + } + database.transaction { val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) val criteria = VaultQueryCriteria() val results = vaultQuerySvc.queryBy(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))) @@ -254,12 +267,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash states sorted by state ref txnId and index`() { + val consumed = mutableSetOf() database.transaction { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 10, 10, Random(0L)) - val consumed = mutableSetOf() + } + database.transaction { services.consumeCash(10.DOLLARS, notary = DUMMY_NOTARY).consumed.forEach { consumed += it.ref.txhash } services.consumeCash(10.DOLLARS, notary = DUMMY_NOTARY).consumed.forEach { consumed += it.ref.txhash } - + } + database.transaction { val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX) val sortBy = Sort(setOf(Sort.SortColumn(sortAttributeTxnId, Sort.Direction.ASC), @@ -279,11 +295,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states for state refs`() { + val stateRefs = + database.transaction { + services.fillWithSomeTestLinearStates(8) + val issuedStates = services.fillWithSomeTestLinearStates(2) + issuedStates.states.map { it.ref }.toList() + } database.transaction { - services.fillWithSomeTestLinearStates(8) - val issuedStates = services.fillWithSomeTestLinearStates(2) - val stateRefs = issuedStates.states.map { it.ref }.toList() - // DOCSTART VaultQueryExample2 val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID) val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last())) @@ -304,8 +322,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - - + } + database.transaction { // default State.Status is UNCONSUMED // DOCSTART VaultQueryExample3 val criteria = VaultQueryCriteria(contractStateTypes = setOf(Cash.State::class.java, DealState::class.java)) @@ -326,7 +344,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.consumeLinearStates(linearStates.states.toList(), DUMMY_NOTARY) services.consumeDeals(dealStates.states.filter { it.state.data.linearId.externalId == "456" }, DUMMY_NOTARY) services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) - + } + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(5) @@ -336,20 +355,22 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `consumed states with count`() { database.transaction { - services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(25.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(4) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(4) - + } + database.transaction { services.consumeCash(75.DOLLARS, notary = DUMMY_NOTARY) - + } + database.transaction { val consumedCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val resultsAfterConsume = vaultQuerySvc.queryBy(consumedCriteria, paging) assertThat(resultsAfterConsume.states).hasSize(3) @@ -368,7 +389,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.consumeLinearStates(linearStates.states.toList(), DUMMY_NOTARY) services.consumeDeals(dealStates.states.filter { it.state.data.linearId.externalId == "456" }, DUMMY_NOTARY) services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) // generates a new change state! - + } + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(17) @@ -378,36 +400,35 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `all states with count`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - - val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + } + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + val paging = PageSpecification(DEFAULT_PAGE_NUM, 10) + database.transaction { val resultsBeforeConsume = vaultQuerySvc.queryBy(criteria, paging) assertThat(resultsBeforeConsume.states).hasSize(1) assertThat(resultsBeforeConsume.totalStatesAvailable).isEqualTo(1) - + } + database.transaction { services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) // consumed 100 (spent), produced 50 (change) - + } + database.transaction { val resultsAfterConsume = vaultQuerySvc.queryBy(criteria, paging) assertThat(resultsAfterConsume.states).hasSize(2) assertThat(resultsAfterConsume.totalStatesAvailable).isEqualTo(2) } } - val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) } - val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public) - @Test fun `unconsumed states by notary`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { // DOCSTART VaultQueryExample4 - val criteria = VaultQueryCriteria(notaryName = listOf(CASH_NOTARY.name)) + val criteria = VaultQueryCriteria(notary = listOf(CASH_NOTARY)) val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample4 assertThat(results.states).hasSize(3) @@ -417,14 +438,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear states for single participant`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BIG_CORP_IDENTITY) services.fillWithSomeTestLinearStates(2, "TEST", participants = listOf(MEGA_CORP, MINI_CORP)) services.fillWithSomeTestDeals(listOf("456"), participants = listOf(MEGA_CORP, BIG_CORP)) services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP)) - + } + database.transaction { val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP)) val results = vaultQuerySvc.queryBy(criteria) - assertThat(results.states).hasSize(3) } } @@ -432,13 +454,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear states for two participants`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BIG_CORP_IDENTITY) services.fillWithSomeTestLinearStates(2, "TEST", participants = listOf(MEGA_CORP, MINI_CORP)) services.fillWithSomeTestDeals(listOf("456"), participants = listOf(MEGA_CORP, BIG_CORP)) - services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP)) - + services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(MEGA_CORP)) + } + database.transaction { // DOCSTART VaultQueryExample5 - val criteria = LinearStateQueryCriteria(participants = listOf(MEGA_CORP, MINI_CORP)) + val criteria = LinearStateQueryCriteria(participants = listOf(BIG_CORP, MINI_CORP)) val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample5 @@ -448,15 +472,17 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states with soft locking`() { + val (lockId1, lockId2) = + database.transaction { + val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 10, 10, Random(0L)).states.toList() + vaultSvc.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) + val lockId1 = UUID.randomUUID() + vaultSvc.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) + val lockId2 = UUID.randomUUID() + vaultSvc.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) + Pair(lockId1, lockId2) + } database.transaction { - - val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 10, 10, Random(0L)).states.toList() - vaultSvc.softLockReserve(UUID.randomUUID(), NonEmptySet.of(issuedStates[1].ref, issuedStates[2].ref, issuedStates[3].ref)) - val lockId1 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId1, NonEmptySet.of(issuedStates[4].ref, issuedStates[5].ref)) - val lockId2 = UUID.randomUUID() - vaultSvc.softLockReserve(lockId2, NonEmptySet.of(issuedStates[6].ref)) - // excluding soft locked states val criteriaExclusive = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.UNLOCKED_ONLY)) val resultsExclusive = vaultQuerySvc.queryBy(criteriaExclusive) @@ -493,11 +519,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator EQUAL`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -508,11 +534,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator NOT EQUAL`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notEqual(GBP.currencyCode) } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -523,11 +549,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator GREATER_THAN`() { database.transaction { - services.fillWithSomeTestCash(1.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(10.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThan(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -538,11 +564,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator GREATER_THAN_OR_EQUAL`() { database.transaction { - services.fillWithSomeTestCash(1.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(10.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.greaterThanOrEqual(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -553,11 +579,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator LESS_THAN`() { database.transaction { - services.fillWithSomeTestCash(1.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(10.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThan(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -568,11 +594,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator LESS_THAN_OR_EQUAL`() { database.transaction { - services.fillWithSomeTestCash(1.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(10.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.lessThanOrEqual(1000L) } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -583,11 +609,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator BETWEEN`() { database.transaction { - services.fillWithSomeTestCash(1.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(10.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.between(500L, 1500L) } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -598,11 +624,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator IN`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val currencies = listOf(CHF.currencyCode, GBP.currencyCode) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.`in`(currencies) } val criteria = VaultCustomQueryCriteria(logicalExpression) @@ -614,11 +640,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator NOT IN`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val currencies = listOf(CHF.currencyCode, GBP.currencyCode) val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notIn(currencies) } val criteria = VaultCustomQueryCriteria(logicalExpression) @@ -630,11 +656,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator LIKE`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.like("%BP") } // GPB val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -645,11 +671,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator NOT LIKE`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notLike("%BP") } // GPB val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -660,11 +686,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator IS_NULL`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.isNull() } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -675,11 +701,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `logical operator NOT_NULL`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val logicalExpression = builder { CashSchemaV1.PersistentCashState::issuerParty.notNull() } val criteria = VaultCustomQueryCriteria(logicalExpression) val results = vaultQuerySvc.queryBy(criteria) @@ -690,13 +716,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `aggregate functions without group clause`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(200.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L)) services.fillWithSomeTestCash(300.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCash(400.POUNDS, notaryServices, DUMMY_NOTARY, 4, 4, Random(0L)) services.fillWithSomeTestCash(500.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) - + } + database.transaction { // DOCSTART VaultQueryExample21 val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum() } val sumCriteria = VaultCustomQueryCriteria(sum) @@ -732,13 +758,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `aggregate functions with single group clause`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(200.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L)) services.fillWithSomeTestCash(300.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCash(400.POUNDS, notaryServices, DUMMY_NOTARY, 4, 4, Random(0L)) services.fillWithSomeTestCash(500.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) - + } + database.transaction { // DOCSTART VaultQueryExample22 val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } val sumCriteria = VaultCustomQueryCriteria(sum) @@ -792,12 +818,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = DUMMY_CASH_ISSUER) services.fillWithSomeTestCash(200.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1)) services.fillWithSomeTestCash(300.POUNDS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) services.fillWithSomeTestCash(400.POUNDS, notaryServices, DUMMY_NOTARY, 4, 4, Random(0L), issuedBy = BOC.ref(2)) - + } + database.transaction { // DOCSTART VaultQueryExample23 val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::issuerParty, CashSchemaV1.PersistentCashState::currency), @@ -833,7 +861,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestLinearStates(2, "JKL") services.fillWithSomeTestLinearStates(3, "ABC") services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { // count fungible assets val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count) @@ -852,18 +881,19 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `aggregate functions count by contract type and state status`() { + val (linearStatesJKL,linearStatesXYZ,dealStates) = + database.transaction { + // create new states + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 10, 10, Random(0L)) + val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") + val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") + services.fillWithSomeTestLinearStates(3, "ABC") + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) + Triple(linearStatesJKL,linearStatesXYZ,dealStates) + } + val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } database.transaction { - // create new states - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 10, 10, Random(0L)) - val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ") - val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL") - services.fillWithSomeTestLinearStates(3, "ABC") - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")) - - // ALL states - // count fungible assets - val count = builder { VaultSchemaV1.VaultStates::recordedTime.count() } val countCriteria = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.ALL) val fungibleStateCount = vaultQuerySvc.queryBy>(countCriteria).otherResults.single() as Long assertThat(fungibleStateCount).isEqualTo(10L) @@ -875,15 +905,18 @@ class VaultQueryTests : TestDependencyInjectionBase() { // count deal states val dealStateCount = vaultQuerySvc.queryBy(countCriteria).otherResults.single() as Long assertThat(dealStateCount).isEqualTo(3L) + } + val cashUpdates = + database.transaction { + // consume some states + services.consumeLinearStates(linearStatesXYZ.states.toList(), DUMMY_NOTARY) + services.consumeLinearStates(linearStatesJKL.states.toList(), DUMMY_NOTARY) + services.consumeDeals(dealStates.states.filter { it.state.data.linearId.externalId == "456" }, DUMMY_NOTARY) + services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) - // consume some states - services.consumeLinearStates(linearStatesXYZ.states.toList(), DUMMY_NOTARY) - services.consumeLinearStates(linearStatesJKL.states.toList(), DUMMY_NOTARY) - services.consumeDeals(dealStates.states.filter { it.state.data.linearId.externalId == "456" }, DUMMY_NOTARY) - val cashUpdates = services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) - - // UNCONSUMED states (default) - + // UNCONSUMED states (default) + } + database.transaction { // count fungible assets val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED) val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy>(countCriteriaUnconsumed).otherResults.single() as Long @@ -919,9 +952,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed states recorded between two time intervals`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 3, 3, Random(0L)) - + } + database.transaction { // DOCSTART VaultQueryExample6 val start = TODAY val end = TODAY.plus(30, ChronoUnit.DAYS) @@ -945,13 +978,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `states consumed after time`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) - + } + database.transaction { val asOfDateTime = TODAY val consumedAfterExpression = TimeCondition( QueryCriteria.TimeInstantType.CONSUMED, ColumnPredicate.BinaryComparison(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, asOfDateTime)) @@ -967,9 +1001,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `all states with paging specification - first page`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 100, 100, Random(0L)) - + } + database.transaction { // DOCSTART VaultQueryExample7 val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -984,9 +1018,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `all states with paging specification - last`() { database.transaction { - services.fillWithSomeTestCash(95.DOLLARS, notaryServices, DUMMY_NOTARY, 95, 95, Random(0L)) - + } + database.transaction { // Last page implies we need to perform a row count for the Query first, // and then re-query for a given offset defined by (count - pageSize) val pagingSpec = PageSpecification(10, 10) @@ -1005,9 +1039,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { expectedEx.expectMessage("Page specification: invalid page number") database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 100, 100, Random(0L)) - + } + database.transaction { val pagingSpec = PageSpecification(0, 10) val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -1022,9 +1056,10 @@ class VaultQueryTests : TestDependencyInjectionBase() { expectedEx.expectMessage("Page specification: invalid page size") database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 100, 100, Random(0L)) - + } + database.transaction { + @Suppress("EXPECTED_CONDITION") val pagingSpec = PageSpecification(DEFAULT_PAGE_NUM, MAX_PAGE_SIZE + 1) // overflow = -2147483648 val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) vaultQuerySvc.queryBy(criteria, paging = pagingSpec) @@ -1038,9 +1073,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { expectedEx.expectMessage("Please specify a `PageSpecification`") database.transaction { - services.fillWithSomeTestCash(201.DOLLARS, notaryServices, DUMMY_NOTARY, 201, 201, Random(0L)) - + } + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) vaultQuerySvc.queryBy(criteria) } @@ -1070,7 +1105,7 @@ class VaultQueryTests : TestDependencyInjectionBase() { assertThat(states).hasSize(20) assertThat(metadata.first().contractStateClassName).isEqualTo("net.corda.testing.contracts.DummyLinearContract\$State") assertThat(metadata.first().status).isEqualTo(Vault.StateStatus.UNCONSUMED) // 0 = UNCONSUMED - assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.contracts.asset.Cash\$State") + assertThat(metadata.last().contractStateClassName).isEqualTo("net.corda.finance.contracts.asset.Cash\$State") assertThat(metadata.last().status).isEqualTo(Vault.StateStatus.CONSUMED) // 1 = CONSUMED } } @@ -1078,11 +1113,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices) services.fillWithSomeTestLinearStates(10) - + } + database.transaction { val results = vaultQuerySvc.queryBy>() assertThat(results.states).hasSize(4) } @@ -1091,12 +1126,16 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `consumed fungible assets`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) + } + database.transaction { services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) + } + database.transaction { services.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices) services.fillWithSomeTestLinearStates(10) - + } + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val results = vaultQuerySvc.queryBy>(criteria) assertThat(results.states).hasSize(2) @@ -1106,10 +1145,10 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash fungible assets`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) - + } + database.transaction { val results = vaultQuerySvc.queryBy() assertThat(results.states).hasSize(3) } @@ -1118,11 +1157,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash fungible assets after spending`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) + } + database.transaction { services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) - // should now have x2 CONSUMED + x2 UNCONSUMED (one spent + one change) - + } + // should now have x2 CONSUMED + x2 UNCONSUMED (one spent + one change) + database.transaction { val results = vaultQuerySvc.queryBy(FungibleAssetQueryCriteria()) assertThat(results.statesMetadata).hasSize(2) assertThat(results.states).hasSize(2) @@ -1132,12 +1173,19 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `consumed cash fungible assets`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) + } + database.transaction { services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) - val linearStates = services.fillWithSomeTestLinearStates(10) + } + val linearStates = + database.transaction { + services.fillWithSomeTestLinearStates(10) + } + database.transaction { services.consumeLinearStates(linearStates.states.toList(), DUMMY_NOTARY) - + } + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(2) @@ -1147,11 +1195,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear heads`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { val results = vaultQuerySvc.queryBy() assertThat(results.states).hasSize(13) } @@ -1160,7 +1208,6 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `consumed linear heads`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) val linearStates = services.fillWithSomeTestLinearStates(2, "TEST") // create 2 states with same externalId services.fillWithSomeTestLinearStates(8) @@ -1169,7 +1216,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.consumeLinearStates(linearStates.states.toList(), DUMMY_NOTARY) services.consumeDeals(dealStates.states.filter { it.state.data.linearId.externalId == "456" }, DUMMY_NOTARY) services.consumeCash(50.DOLLARS, notary = DUMMY_NOTARY) - + } + database.transaction { val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(3) @@ -1180,13 +1228,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear heads for linearId without external Id`() { + val issuedStates = + database.transaction { + services.fillWithSomeTestLinearStates(10) + } database.transaction { - - val issuedStates = services.fillWithSomeTestLinearStates(10) - // DOCSTART VaultQueryExample8 - val linearIds = issuedStates.states.map { it.state.data.linearId.id }.toList() - val criteria = LinearStateQueryCriteria(uuid = listOf(linearIds.first(), linearIds.last())) + val linearIds = issuedStates.states.map { it.state.data.linearId }.toList() + val criteria = LinearStateQueryCriteria(linearId = listOf(linearIds.first(), linearIds.last())) val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample8 assertThat(results.states).hasSize(2) @@ -1194,34 +1243,54 @@ class VaultQueryTests : TestDependencyInjectionBase() { } @Test - fun `unconsumed linear heads for linearId with external Id`() { + fun `unconsumed linear heads by linearId`() { + val (linearState1, linearState3) = + database.transaction { + val linearState1 = services.fillWithSomeTestLinearStates(1, "ID1") + services.fillWithSomeTestLinearStates(1, "ID2") + val linearState3 = services.fillWithSomeTestLinearStates(1, "ID3") + Pair(linearState1, linearState3) + } database.transaction { - - val linearState1 = services.fillWithSomeTestLinearStates(1, "ID1") - services.fillWithSomeTestLinearStates(1, "ID2") - val linearState3 = services.fillWithSomeTestLinearStates(1, "ID3") - - val linearIds = listOf(linearState1.states.first().state.data.linearId.id, linearState3.states.first().state.data.linearId.id) - val criteria = LinearStateQueryCriteria(uuid = linearIds) + val linearIds = listOf(linearState1.states.first().state.data.linearId, linearState3.states.first().state.data.linearId) + val criteria = LinearStateQueryCriteria(linearId = linearIds) val results = vaultQuerySvc.queryBy(criteria) assertThat(results.states).hasSize(2) } } @Test - fun `all linear states for a given id`() { + fun `unconsumed linear heads for linearId by external Id`() { + val (linearState1, linearState3) = + database.transaction { + val linearState1 = services.fillWithSomeTestLinearStates(1, "ID1") + services.fillWithSomeTestLinearStates(1, "ID2") + val linearState3 = services.fillWithSomeTestLinearStates(1, "ID3") + Pair(linearState1, linearState3) + } database.transaction { + val externalIds = listOf(linearState1.states.first().state.data.linearId.externalId!!, linearState3.states.first().state.data.linearId.externalId!!) + val criteria = LinearStateQueryCriteria(externalId = externalIds) + val results = vaultQuerySvc.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } - val txns = services.fillWithSomeTestLinearStates(1, "TEST") - val linearState = txns.states.first() - val linearId = linearState.state.data.linearId - services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - + @Test + fun `all linear states for a given linear id`() { + val linearId = + database.transaction { + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + linearState.state.data.linearId + } + database.transaction { // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" // DOCSTART VaultQueryExample9 - val linearStateCriteria = LinearStateQueryCriteria(uuid = listOf(linearId.id), status = Vault.StateStatus.ALL) + val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val results = vaultQuerySvc.queryBy(linearStateCriteria and vaultCriteria) // DOCEND VaultQueryExample9 @@ -1231,14 +1300,16 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `all linear states for a given id sorted by uuid`() { + val linearStates = + database.transaction { + val txns = services.fillWithSomeTestLinearStates(2, "TEST") + val linearStates = txns.states.toList() + services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference + linearStates + } database.transaction { - - val txns = services.fillWithSomeTestLinearStates(2, "TEST") - val linearStates = txns.states.toList() - services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearStates(linearStates, DUMMY_NOTARY) // consume current and produce new state reference - // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" val linearStateCriteria = LinearStateQueryCriteria(uuid = linearStates.map { it.state.data.linearId.id }, status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) @@ -1251,13 +1322,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { } @Test - fun `unconsumed linear states sorted by linear state attribute`() { + fun `unconsumed linear states sorted by external id`() { database.transaction { - services.fillWithSomeTestLinearStates(1, externalId = "111") services.fillWithSomeTestLinearStates(2, externalId = "222") services.fillWithSomeTestLinearStates(3, externalId = "333") - + } + database.transaction { val vaultCriteria = VaultQueryCriteria() val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC))) @@ -1269,12 +1340,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed deal states sorted`() { + val uid = + database.transaction { + val linearStates = services.fillWithSomeTestLinearStates(10) + services.fillWithSomeTestDeals(listOf("123", "456", "789")) + linearStates.states.first().state.data.linearId.id + } database.transaction { - - val linearStates = services.fillWithSomeTestLinearStates(10) - val uid = linearStates.states.first().state.data.linearId.id - services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val linearStateCriteria = LinearStateQueryCriteria(uuid = listOf(uid)) val dealStateCriteria = LinearStateQueryCriteria(externalId = listOf("123", "456", "789")) val compositeCriteria = linearStateCriteria or dealStateCriteria @@ -1282,19 +1354,19 @@ class VaultQueryTests : TestDependencyInjectionBase() { val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.EXTERNAL_ID), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(compositeCriteria, sorting = sorting) - assertThat(results.statesMetadata).hasSize(13) - assertThat(results.states).hasSize(13) + assertThat(results.statesMetadata).hasSize(4) + assertThat(results.states).hasSize(4) } } @Test fun `unconsumed linear states sorted by custom attribute`() { database.transaction { - services.fillWithSomeTestLinearStates(1, linearString = "111") services.fillWithSomeTestLinearStates(2, linearString = "222") services.fillWithSomeTestLinearStates(3, linearString = "333") - + } + database.transaction { val vaultCriteria = VaultQueryCriteria() val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Custom(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java, "linearString"), Sort.Direction.DESC))) @@ -1305,17 +1377,19 @@ class VaultQueryTests : TestDependencyInjectionBase() { } @Test - fun `return consumed linear states for a given id`() { + fun `return consumed linear states for a given linear id`() { + val txns = + database.transaction { + val txns = services.fillWithSomeTestLinearStates(1, "TEST") + val linearState = txns.states.first() + val linearState2 = services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference + val linearState3 = services.evolveLinearState(linearState2, DUMMY_NOTARY) // consume current and produce new state reference + services.evolveLinearState(linearState3, DUMMY_NOTARY) // consume current and produce new state reference + txns + } database.transaction { - - val txns = services.fillWithSomeTestLinearStates(1, "TEST") - val linearState = txns.states.first() - val linearState2 = services.evolveLinearState(linearState, DUMMY_NOTARY) // consume current and produce new state reference - val linearState3 = services.evolveLinearState(linearState2, DUMMY_NOTARY) // consume current and produce new state reference - services.evolveLinearState(linearState3, DUMMY_NOTARY) // consume current and produce new state reference - // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" - val linearStateCriteria = LinearStateQueryCriteria(uuid = txns.states.map { it.state.data.linearId.id }, status = Vault.StateStatus.CONSUMED) + val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }, status = Vault.StateStatus.CONSUMED) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) @@ -1329,9 +1403,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed deals`() { database.transaction { - services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { val results = vaultQuerySvc.queryBy() assertThat(results.states).hasSize(3) } @@ -1340,9 +1414,9 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed deals for ref`() { database.transaction { - services.fillWithSomeTestDeals(listOf("123", "456", "789")) - + } + database.transaction { // DOCSTART VaultQueryExample10 val criteria = LinearStateQueryCriteria(externalId = listOf("456", "789")) val results = vaultQuerySvc.queryBy(criteria) @@ -1355,11 +1429,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `latest unconsumed deals for ref`() { database.transaction { - services.fillWithSomeTestLinearStates(2, "TEST") services.fillWithSomeTestDeals(listOf("456")) services.fillWithSomeTestDeals(listOf("123", "789")) - + } + database.transaction { val all = vaultQuerySvc.queryBy() all.states.forEach { println(it.state) } @@ -1371,15 +1445,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `latest unconsumed deals with party`() { - + val parties = listOf(MINI_CORP) database.transaction { - - val parties = listOf(MEGA_CORP) - services.fillWithSomeTestLinearStates(2, "TEST") services.fillWithSomeTestDeals(listOf("456"), parties) services.fillWithSomeTestDeals(listOf("123", "789")) - + } + database.transaction { // DOCSTART VaultQueryExample11 val criteria = LinearStateQueryCriteria(participants = parties) val results = vaultQuerySvc.queryBy(criteria) @@ -1394,13 +1466,15 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets for specific issuer party and refs`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), ref = OpaqueBytes.of(1)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(2)), ref = OpaqueBytes.of(2)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(3)), ref = OpaqueBytes.of(3)) - - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC), + } + database.transaction { + val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC), issuerRef = listOf(BOC.ref(1).reference, BOC.ref(2).reference)) val results = vaultQuerySvc.queryBy>(criteria) assertThat(results.states).hasSize(2) @@ -1423,11 +1497,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { val chfCashIssuerServices = MockServices(chfCashIssuerKey) database.transaction { + services.fillWithSomeTestCash(100.POUNDS, gbpCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (gbpCashIssuer)) services.fillWithSomeTestCash(100.DOLLARS, usdCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (usdCashIssuer)) services.fillWithSomeTestCash(100.SWISS_FRANCS, chfCashIssuerServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (chfCashIssuer)) - - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(gbpCashIssuer.party, usdCashIssuer.party)) + } + database.transaction { + val criteria = FungibleAssetQueryCriteria(issuer = listOf(gbpCashIssuer.party, usdCashIssuer.party)) val results = vaultQuerySvc.queryBy>(criteria) assertThat(results.states).hasSize(2) } @@ -1436,34 +1512,33 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets by owner`() { database.transaction { - - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1)) + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = BOC.ref(1)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), - issuedBy = MEGA_CORP.ref(0), ownedBy = (MEGA_CORP)) - + issuedBy = MEGA_CORP.ref(0), ownedBy = (MINI_CORP)) + } + database.transaction { val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP)) val results = vaultQuerySvc.queryBy>(criteria) - assertThat(results.states).hasSize(1) + assertThat(results.states).hasSize(1) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) } } - @Test fun `unconsumed fungible states for owners`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, CASH_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = MEGA_CORP.ref(0), ownedBy = (MEGA_CORP)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = BOC.ref(0), ownedBy = (MINI_CORP)) // irrelevant to this vault - + } + database.transaction { // DOCSTART VaultQueryExample5.2 val criteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP, BOC)) val results = vaultQuerySvc.queryBy(criteria) // DOCEND VaultQueryExample5.2 - assertThat(results.states).hasSize(1) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) + assertThat(results.states).hasSize(2) // can only be 1 owner of a node (MEGA_CORP in this MockServices setup) } } @@ -1471,12 +1546,12 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets for single currency`() { database.transaction { - services.fillWithSomeTestLinearStates(10) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) - + } + database.transaction { // DOCSTART VaultQueryExample12 val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) } val criteria = VaultCustomQueryCriteria(ccyIndex) @@ -1490,10 +1565,10 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash balance for single currency`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(200.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L)) - + } + database.transaction { val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } val sumCriteria = VaultCustomQueryCriteria(sum) @@ -1511,14 +1586,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed cash balances for all currencies`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(200.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L)) services.fillWithSomeTestCash(300.POUNDS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCash(400.POUNDS, notaryServices, DUMMY_NOTARY, 4, 4, Random(0L)) services.fillWithSomeTestCash(500.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) services.fillWithSomeTestCash(600.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 6, 6, Random(0L)) - + } + database.transaction { val ccyIndex = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) } val criteria = VaultCustomQueryCriteria(ccyIndex) val results = vaultQuerySvc.queryBy>(criteria) @@ -1536,12 +1611,12 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets for quantity greater than`() { database.transaction { - services.fillWithSomeTestCash(10.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) services.fillWithSomeTestCash(25.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(50.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) - + } + database.transaction { // DOCSTART VaultQueryExample13 val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(2500L) }) val results = vaultQuerySvc.queryBy(fungibleAssetCriteria) @@ -1554,12 +1629,14 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets for issuer party`() { database.transaction { + identitySvc.verifyAndRegisterIdentity(BOC_IDENTITY) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1))) - + } + database.transaction { // DOCSTART VaultQueryExample14 - val criteria = FungibleAssetQueryCriteria(issuerPartyName = listOf(BOC)) + val criteria = FungibleAssetQueryCriteria(issuer = listOf(BOC)) val results = vaultQuerySvc.queryBy>(criteria) // DOCEND VaultQueryExample14 @@ -1570,12 +1647,12 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed fungible assets for single currency and quantity greater than`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(50.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(GBP.currencyCode) } val customCriteria = VaultCustomQueryCriteria(ccyIndex) val fungibleAssetCriteria = FungibleAssetQueryCriteria(quantity = builder { greaterThan(5000L) }) @@ -1591,7 +1668,6 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `custom query using JPA - commercial paper schema V1 single attribute`() { database.transaction { - val issuance = MEGA_CORP.ref(1) // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned by itself. @@ -1614,7 +1690,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { notaryServices.addSignature(stx, DUMMY_NOTARY_KEY.public) } services.recordTransactions(commercialPaper2) - + } + database.transaction { val ccyIndex = builder { CommercialPaperSchemaV1.PersistentCommercialPaperState::currency.equal(USD.currencyCode) } val criteria1 = QueryCriteria.VaultCustomQueryCriteria(ccyIndex) @@ -1629,7 +1706,6 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `custom query using JPA - commercial paper schema V1 - multiple attributes`() { database.transaction { - val issuance = MEGA_CORP.ref(1) // MegaCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned by itself. @@ -1653,7 +1729,8 @@ class VaultQueryTests : TestDependencyInjectionBase() { } commercialPaper2.verifyRequiredSignatures() services.recordTransactions(commercialPaper2) - + } + database.transaction { val result = builder { val ccyIndex = CommercialPaperSchemaV1.PersistentCommercialPaperState::currency.equal(USD.currencyCode) @@ -1666,8 +1743,6 @@ class VaultQueryTests : TestDependencyInjectionBase() { vaultQuerySvc.queryBy(criteria1.and(criteria3).and(criteria2)) } - - Assertions.assertThat(result.states).hasSize(1) Assertions.assertThat(result.statesMetadata).hasSize(1) } @@ -1676,11 +1751,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `query attempting to use unregistered schema`() { database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.SWISS_FRANCS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { // CashSchemaV3 NOT registered with NodeSchemaService val logicalExpression = builder { SampleCashSchemaV3.PersistentCashState::currency.equal(GBP.currencyCode) } val criteria = VaultCustomQueryCriteria(logicalExpression) @@ -1696,14 +1771,13 @@ class VaultQueryTests : TestDependencyInjectionBase() { // specifying Query on Cash contract state attributes @Test fun `custom - all cash states with amount of currency greater or equal than`() { - database.transaction { - services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(10.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) services.fillWithSomeTestCash(1.DOLLARS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) - + } + database.transaction { // DOCSTART VaultQueryExample20 val generalCriteria = VaultQueryCriteria(Vault.StateStatus.ALL) @@ -1726,17 +1800,17 @@ class VaultQueryTests : TestDependencyInjectionBase() { // specifying Query on Linear state attributes @Test fun `unconsumed linear heads for linearId between two timestamps`() { + val start = Instant.now() + val end = start.plus(1, ChronoUnit.SECONDS) + database.transaction { - - val start = Instant.now() - val end = start.plus(1, ChronoUnit.SECONDS) - services.fillWithSomeTestLinearStates(1, "TEST") sleep(1000) services.fillWithSomeTestLinearStates(1, "TEST") // 2 unconsumed states with same external ID - + } + database.transaction { val recordedBetweenExpression = TimeCondition(TimeInstantType.RECORDED, builder { between(start, end) }) val basicCriteria = VaultQueryCriteria(timeCondition = recordedBetweenExpression) @@ -1750,12 +1824,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear heads for a given external id`() { database.transaction { - services.fillWithSomeTestLinearStates(1, "TEST1") services.fillWithSomeTestLinearStates(1, "TEST2") - + } + database.transaction { // 2 unconsumed states with same external ID - val externalIdCondition = builder { VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") } val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) @@ -1768,16 +1841,16 @@ class VaultQueryTests : TestDependencyInjectionBase() { // specifying Query on Linear state attributes @Test fun `unconsumed linear heads for linearId between two timestamps for a given external id`() { + val start = Instant.now() + val end = start.plus(1, ChronoUnit.SECONDS) + database.transaction { - - val start = Instant.now() - val end = start.plus(1, ChronoUnit.SECONDS) - services.fillWithSomeTestLinearStates(1, "TEST1") services.fillWithSomeTestLinearStates(1, "TEST2") sleep(1000) services.fillWithSomeTestLinearStates(1, "TEST3") - + } + database.transaction { // 2 unconsumed states with same external ID val results = builder { @@ -1798,17 +1871,18 @@ class VaultQueryTests : TestDependencyInjectionBase() { // specifying Query on Linear state attributes @Test fun `unconsumed linear heads for a given external id or uuid`() { + val uuid = + database.transaction { + services.fillWithSomeTestLinearStates(1, "TEST1") + val aState = services.fillWithSomeTestLinearStates(1, "TEST2").states + services.consumeLinearStates(aState.toList(), DUMMY_NOTARY) + services.fillWithSomeTestLinearStates(1, "TEST1").states.first().state.data.linearId.id + + // 2 unconsumed states with same external ID, 1 consumed with different external ID + } database.transaction { - - services.fillWithSomeTestLinearStates(1, "TEST1") - val aState = services.fillWithSomeTestLinearStates(1, "TEST2").states - services.consumeLinearStates(aState.toList(), DUMMY_NOTARY) - val uuid = services.fillWithSomeTestLinearStates(1, "TEST3").states.first().state.data.linearId.id - - // 2 unconsumed states with same external ID, 1 with different external ID - val results = builder { - val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") + val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST1") val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) val uuidCondition = VaultSchemaV1.VaultLinearStates::uuid.equal(uuid) @@ -1817,28 +1891,64 @@ class VaultQueryTests : TestDependencyInjectionBase() { val criteria = externalIdCustomCriteria or uuidCustomCriteria vaultQuerySvc.queryBy(criteria) } - assertThat(results.statesMetadata).hasSize(3) - assertThat(results.states).hasSize(3) + assertThat(results.statesMetadata).hasSize(2) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `unconsumed linear heads for single participant`() { + database.transaction { + identitySvc.verifyAndRegisterIdentity(ALICE_IDENTITY) + services.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE)) + services.fillWithSomeTestLinearStates(1) + services.fillWithSomeTestLinearStates(1, "TEST3") + } + database.transaction { + val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE)) + val results = vaultQuerySvc.queryBy(linearStateCriteria) + + assertThat(results.states).hasSize(1) + assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") + } + } + + @Test + fun `unconsumed linear heads for multiple participants`() { + database.transaction { + identitySvc.verifyAndRegisterIdentity(ALICE_IDENTITY) + identitySvc.verifyAndRegisterIdentity(BOB_IDENTITY) + identitySvc.verifyAndRegisterIdentity(CHARLIE_IDENTITY) + + services.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE,BOB,CHARLIE)) + services.fillWithSomeTestLinearStates(1) + services.fillWithSomeTestLinearStates(1, "TEST3") + } + database.transaction { + val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE,BOB,CHARLIE)) + val results = vaultQuerySvc.queryBy(linearStateCriteria) + + assertThat(results.states).hasSize(1) + assertThat(results.states[0].state.data.linearId.externalId).isEqualTo("TEST1") } } @Test fun `unconsumed linear heads where external id is null`() { database.transaction { - services.fillWithSomeTestLinearStates(1, "TEST1") services.fillWithSomeTestLinearStates(1) services.fillWithSomeTestLinearStates(1, "TEST3") // 3 unconsumed states (one without an external ID) - + } + database.transaction { val results = builder { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.isNull() val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) vaultQuerySvc.queryBy(externalIdCustomCriteria) } - assertThat(results.states).hasSize(1) } } @@ -1846,24 +1956,51 @@ class VaultQueryTests : TestDependencyInjectionBase() { @Test fun `unconsumed linear heads where external id is not null`() { database.transaction { - services.fillWithSomeTestLinearStates(1, "TEST1") services.fillWithSomeTestLinearStates(1) services.fillWithSomeTestLinearStates(1, "TEST3") // 3 unconsumed states (two with an external ID) - + } + database.transaction { val results = builder { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.notNull() val externalIdCustomCriteria = VaultCustomQueryCriteria(externalIdCondition) vaultQuerySvc.queryBy(externalIdCustomCriteria) } - assertThat(results.states).hasSize(2) } } + @Test + fun `enriched and overridden composite query handles defaults correctly`() { + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 2, 2, Random(0L)) + services.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices) + services.fillWithSomeTestLinearStates(1, "ABC") + services.fillWithSomeTestDeals(listOf("123")) + } + + database.transaction { + // Base criteria + val baseCriteria = VaultQueryCriteria(notary = listOf(DUMMY_NOTARY), + status = Vault.StateStatus.CONSUMED) + + // Enrich and override QueryCriteria with additional default attributes (such as soft locks) + val enrichedCriteria = VaultQueryCriteria(contractStateTypes = setOf(DealState::class.java), // enrich + softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(UUID.randomUUID())), + status = Vault.StateStatus.UNCONSUMED) // override + // Sorting + val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) + val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) + + // Execute query + val results = services.vaultQueryService.queryBy>(baseCriteria and enrichedCriteria, sorter).states + assertThat(results).hasSize(4) + } + } + /** * Dynamic trackBy() tests */ @@ -1872,30 +2009,27 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun trackCashStates_unconsumed() { val updates = database.transaction { - + // DOCSTART VaultQueryExample15 + vaultQuerySvc.trackBy().updates // UNCONSUMED default + // DOCEND VaultQueryExample15 + } + val (linearStates,dealStates) = + database.transaction { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) val linearStates = services.fillWithSomeTestLinearStates(10).states val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - - // DOCSTART VaultQueryExample15 - val (snapshot, updates) = vaultQuerySvc.trackBy() // UNCONSUMED default - // DOCEND VaultQueryExample15 - - assertThat(snapshot.states).hasSize(5) - assertThat(snapshot.statesMetadata).hasSize(5) - // add more cash services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) // add another deal services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - - // consume stuff - services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) - services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) - services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) - - updates + Pair(linearStates,dealStates) } + database.transaction { + // consume stuff + services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) + services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) + services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) + } updates.expectEvents { sequence( @@ -1917,7 +2051,11 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun trackCashStates_consumed() { val updates = database.transaction { - + val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) + vaultQuerySvc.trackBy(criteria).updates + } + val (linearStates,dealStates) = + database.transaction { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) val linearStates = services.fillWithSomeTestLinearStates(10).states val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states @@ -1926,23 +2064,18 @@ class VaultQueryTests : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) // add another deal services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - - // consume stuff - services.consumeCash(100.POUNDS, notary = DUMMY_NOTARY) - - val criteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) - val (snapshot, updates) = vaultQuerySvc.trackBy(criteria) - - assertThat(snapshot.states).hasSize(1) - assertThat(snapshot.statesMetadata).hasSize(1) - - // consume more stuff - services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) - services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) - services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) - - updates + Pair(linearStates,dealStates) } + database.transaction { + // consume stuff + services.consumeCash(100.POUNDS, notary = DUMMY_NOTARY) + } + database.transaction { + // consume more stuff + services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) + services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) + services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) + } updates.expectEvents { sequence( @@ -1964,32 +2097,30 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun trackCashStates_all() { val updates = database.transaction { - + val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) + vaultQuerySvc.trackBy(criteria).updates + } + val (linearStates,dealStates) = + database.transaction { services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 5, 5, Random(0L)) val linearStates = services.fillWithSomeTestLinearStates(10).states val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // add more cash services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) // add another deal services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - - // consume stuff - services.consumeCash(99.POUNDS, notary = DUMMY_NOTARY) - - val criteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) - val (snapshot, updates) = vaultQuerySvc.trackBy(criteria) - - assertThat(snapshot.states).hasSize(7) - assertThat(snapshot.statesMetadata).hasSize(7) - - // consume more stuff - services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) - services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) - services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) - - updates + Pair(linearStates,dealStates) } + database.transaction { + // consume stuff + services.consumeCash(99.POUNDS, notary = DUMMY_NOTARY) + } + database.transaction { + // consume more stuff + services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) + services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) + services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) + } updates.expectEvents { sequence( @@ -2021,31 +2152,29 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun trackLinearStates() { val updates = database.transaction { - - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) - val linearStates = services.fillWithSomeTestLinearStates(10).states - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // DOCSTART VaultQueryExample16 val (snapshot, updates) = vaultQuerySvc.trackBy() // DOCEND VaultQueryExample16 - - - assertThat(snapshot.states).hasSize(13) - assertThat(snapshot.statesMetadata).hasSize(13) - + assertThat(snapshot.states).hasSize(0) + updates + } + val (linearStates,dealStates) = + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states // add more cash services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) // add another deal services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - - // consume stuff - services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) - services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) - services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) - - updates + Pair(linearStates,dealStates) } + database.transaction { + // consume stuff + services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) + services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) + services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) + } updates.expectEvents { sequence( @@ -2072,30 +2201,29 @@ class VaultQueryTests : TestDependencyInjectionBase() { fun trackDealStates() { val updates = database.transaction { - - services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) - val linearStates = services.fillWithSomeTestLinearStates(10).states - val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states - // DOCSTART VaultQueryExample17 val (snapshot, updates) = vaultQuerySvc.trackBy() // DOCEND VaultQueryExample17 - - assertThat(snapshot.states).hasSize(3) - assertThat(snapshot.statesMetadata).hasSize(3) - - // add more cash + assertThat(snapshot.states).hasSize(0) + updates + } + val (linearStates,dealStates) = + database.transaction { + services.fillWithSomeTestCash(100.DOLLARS, notaryServices, DUMMY_NOTARY, 3, 3, Random(0L)) + val linearStates = services.fillWithSomeTestLinearStates(10).states + val dealStates = services.fillWithSomeTestDeals(listOf("123", "456", "789")).states + // add more cash services.fillWithSomeTestCash(100.POUNDS, notaryServices, DUMMY_NOTARY, 1, 1, Random(0L)) // add another deal services.fillWithSomeTestDeals(listOf("SAMPLE DEAL")) - - // consume stuff - services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) - services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) - services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) - - updates + Pair(linearStates,dealStates) } + database.transaction { + // consume stuff + services.consumeCash(100.DOLLARS, notary = DUMMY_NOTARY) + services.consumeDeals(dealStates.toList(), DUMMY_NOTARY) + services.consumeLinearStates(linearStates.toList(), DUMMY_NOTARY) + } updates.expectEvents { sequence( diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index 0b35c7a775..66bf35ba86 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -1,10 +1,8 @@ package net.corda.node.services.vault -import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY -import net.corda.contracts.getCashBalance -import net.corda.core.contracts.* +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryService @@ -13,6 +11,11 @@ import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.* +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER +import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY +import net.corda.finance.contracts.getCashBalance import net.corda.node.utilities.CordaPersistence import net.corda.testing.* import net.corda.testing.contracts.* @@ -58,7 +61,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() { database.transaction { // Fix the PRNG so that we get the same splits every time. services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER) - + } + database.transaction { val w = vaultQuery.queryBy().states assertEquals(3, w.size) @@ -74,25 +78,31 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Test fun `issue and spend total correctly and irrelevant ignored`() { val megaCorpServices = MockServices(MEGA_CORP_KEY) + val freshKey = services.keyManagementService.freshKey() + val usefulTX = + database.transaction { + // A tx that sends us money. + val usefulBuilder = TransactionBuilder(null) + Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) + megaCorpServices.signInitialTransaction(usefulBuilder) + } database.transaction { - // A tx that sends us money. - val freshKey = services.keyManagementService.freshKey() - val usefulBuilder = TransactionBuilder(null) - Cash().generateIssue(usefulBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) - val usefulTX = megaCorpServices.signInitialTransaction(usefulBuilder) - assertEquals(0.DOLLARS, services.getCashBalance(USD)) services.recordTransactions(usefulTX) - - // A tx that spends our money. - val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) - Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) - val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) - val spendTX = notaryServices.addSignature(spendPTX) - + } + val spendTX = + database.transaction { + // A tx that spends our money. + val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY) + Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB) + val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey) + notaryServices.addSignature(spendPTX) + } + database.transaction { assertEquals(100.DOLLARS, services.getCashBalance(USD)) - + } + database.transaction { // A tx that doesn't send us anything. val irrelevantBuilder = TransactionBuilder(DUMMY_NOTARY) Cash().generateIssue(irrelevantBuilder, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY) @@ -101,12 +111,15 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val irrelevantTX = notaryServices.addSignature(irrelevantPTX) services.recordTransactions(irrelevantTX) + } + database.transaction { assertEquals(100.DOLLARS, services.getCashBalance(USD)) + } + database.transaction { services.recordTransactions(spendTX) - + } + database.transaction { assertEquals(20.DOLLARS, services.getCashBalance(USD)) - - // TODO: Flesh out these tests as needed. } } @@ -120,7 +133,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 10, 10, Random(0L), ownedBy = AnonymousParty(freshKey), issuedBy = MEGA_CORP.ref(1)) println("Cash balance: ${services.getCashBalance(USD)}") - + } + database.transaction { assertThat(vaultQuery.queryBy().states).hasSize(10) assertThat(vaultQuery.queryBy(criteriaLocked).states).hasSize(0) } @@ -219,6 +233,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY).apply { addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + addCommand(dummyCommand(notaryServices.legalIdentityKey)) } val dummyIssue = notaryServices.signInitialTransaction(dummyIssueBuilder) @@ -230,34 +245,38 @@ class VaultWithCashTest : TestDependencyInjectionBase() { @Test fun `sequencing LinearStates works`() { - database.transaction { - val freshKey = services.keyManagementService.freshKey() - val freshIdentity = AnonymousParty(freshKey) + val freshKey = services.keyManagementService.freshKey() + val freshIdentity = AnonymousParty(freshKey) + val linearId = UniqueIdentifier() - val linearId = UniqueIdentifier() - - // Issue a linear state + val dummyIssue = + database.transaction { // Issue a linear state val dummyIssueBuilder = TransactionBuilder(notary = DUMMY_NOTARY) - dummyIssueBuilder.addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))).addCommand(dummyCommand(notaryServices.legalIdentityKey)) val dummyIssuePtx = notaryServices.signInitialTransaction(dummyIssueBuilder) val dummyIssue = services.addSignature(dummyIssuePtx) - dummyIssue.toLedgerTransaction(services).verify() + dummyIssue.toLedgerTransaction(services).verify() - services.recordTransactions(dummyIssue) + services.recordTransactions(dummyIssue) + dummyIssue + } + database.transaction { assertThat(vaultQuery.queryBy().states).hasSize(1) // Move the same state - val dummyMoveBuilder = TransactionBuilder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) - addInputState(dummyIssue.tx.outRef(0)) - } + val dummyMoveBuilder = TransactionBuilder(notary = DUMMY_NOTARY) + .addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + .addInputState(dummyIssue.tx.outRef(0)) + .addCommand(dummyCommand(notaryServices.legalIdentityKey)) val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) dummyIssue.toLedgerTransaction(services).verify() services.recordTransactions(dummyMove) + } + database.transaction { assertThat(vaultQuery.queryBy().states).hasSize(1) } } @@ -270,10 +289,15 @@ class VaultWithCashTest : TestDependencyInjectionBase() { services.fillWithSomeTestCash(100.DOLLARS, issuerServices, DUMMY_NOTARY, 3, 3, Random(0L), ownedBy = AnonymousParty(freshKey)) services.fillWithSomeTestCash(100.SWISS_FRANCS, issuerServices, DUMMY_NOTARY, 2, 2, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, issuerServices, DUMMY_NOTARY, 1, 1, Random(0L)) + } + database.transaction { val cash = vaultQuery.queryBy().states cash.forEach { println(it.state.data.amount) } - + } + database.transaction { services.fillWithSomeTestDeals(listOf("123", "456", "789")) + } + database.transaction { val deals = vaultQuery.queryBy().states deals.forEach { println(it.state.data.linearId.externalId!!) } } @@ -285,7 +309,8 @@ class VaultWithCashTest : TestDependencyInjectionBase() { val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder) val spendTX = services.addSignature(spendPTX, freshKey) services.recordTransactions(spendTX) - + } + database.transaction { val consumedStates = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states assertEquals(3, consumedStates.count()) @@ -295,17 +320,21 @@ class VaultWithCashTest : TestDependencyInjectionBase() { } @Test - fun `consuming multiple contract state types in same transaction`() { + fun `consuming multiple contract state types`() { val freshKey = services.keyManagementService.freshKey() val freshIdentity = AnonymousParty(freshKey) database.transaction { - services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val deals = vaultQuery.queryBy().states - deals.forEach { println(it.state.data.linearId.externalId!!) } - + } + val deals = + database.transaction { + vaultQuery.queryBy().states + } + database.transaction { services.fillWithSomeTestLinearStates(3) + } + database.transaction { val linearStates = vaultQuery.queryBy().states linearStates.forEach { println(it.state.data.linearId) } @@ -315,13 +344,14 @@ class VaultWithCashTest : TestDependencyInjectionBase() { addOutputState(DummyDealContract.State(ref = "999", participants = listOf(freshIdentity))) addInputState(linearStates.first()) addInputState(deals.first()) + addCommand(dummyCommand(notaryServices.legalIdentityKey)) } val dummyMove = notaryServices.signInitialTransaction(dummyMoveBuilder) - dummyMove.toLedgerTransaction(services).verify() services.recordTransactions(dummyMove) - + } + database.transaction { val consumedStates = vaultQuery.queryBy(VaultQueryCriteria(status = Vault.StateStatus.CONSUMED)).states assertEquals(2, consumedStates.count()) diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt index 0d99e3f3b8..900c00e482 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -21,7 +21,7 @@ class ObservablesTests { val toBeClosed = mutableListOf() fun createDatabase(): CordaPersistence { - val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + val database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) toBeClosed += database return database } diff --git a/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt index 69ada450ff..2c528cbfd5 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/X509UtilitiesTest.kt @@ -398,7 +398,7 @@ class X509UtilitiesTest { @Test fun `serialize - deserialize X509CertififcateHolder`() { - val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, @@ -413,7 +413,7 @@ class X509UtilitiesTest { @Test fun `serialize - deserialize X509CertPath`() { - val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme(this)) } + val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) } val context = SerializationContextImpl(KryoHeaderV0_1, javaClass.classLoader, AllWhitelist, diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index 51de115d1f..da5569e280 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -9,12 +9,12 @@ import net.corda.core.crypto.cert import net.corda.core.crypto.commonName import net.corda.core.internal.exists import net.corda.core.internal.toTypedArray +import net.corda.core.internal.toX509CertHolder import net.corda.node.utilities.X509Utilities import net.corda.node.utilities.loadKeyStore import net.corda.testing.ALICE import net.corda.testing.getTestX509Name import net.corda.testing.testNodeConfiguration -import org.bouncycastle.cert.X509CertificateHolder import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -68,7 +68,7 @@ class NetworkRegistrationHelperTest { assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_CA) assertEquals(3, certificateChain.size) - assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { it.toX509CertHolder().subject.commonName }) } sslKeystore.run { @@ -78,7 +78,7 @@ class NetworkRegistrationHelperTest { assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_TLS)) val certificateChain = getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) assertEquals(4, certificateChain.size) - assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { X509CertificateHolder(it.encoded).subject.commonName }) + assertEquals(listOf("CORDA_CLIENT_CA", "CORDA_CLIENT_CA", "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA"), certificateChain.map { it.toX509CertHolder().subject.commonName }) } trustStore.run { diff --git a/samples/README.md b/samples/README.md index 49255bfe41..fff62b2e0b 100644 --- a/samples/README.md +++ b/samples/README.md @@ -6,6 +6,6 @@ Please refer to `README.md` in the individual project folders. There are the fo * **irs-demo** A demo showing two nodes agreeing to an interest rate swap and doing fixings using an oracle. * **trader-demo** A simple driver for exercising the two party trading flow. In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for commercial paper. The seller learns that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits and the buyer goes back to waiting. The buyer will sell as much CP as he can! **We recommend starting with this demo.** * **Network-visualiser** A tool that uses a simulation to visualise the interaction and messages between nodes on the Corda network. Currently only works for the IRS demo. -* **simm-valudation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio. +* **simm-valuation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio. * **notary-demo** A simple demonstration of a node getting multiple transactions notarised by a single or distributed (Raft or BFT SMaRt) notary. * **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash) diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 6a8491a923..050eb95246 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -66,7 +66,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } task integrationTest(type: Test, dependsOn: []) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index b1bea3b8ab..a8e2787c3a 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -7,7 +7,7 @@ import net.corda.testing.DUMMY_BANK_A import net.corda.testing.DUMMY_BANK_B import net.corda.testing.DUMMY_NOTARY import net.corda.testing.driver.driver -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import org.junit.Test diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index 8c03f061f4..adc945e56b 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -177,9 +177,6 @@ private fun printHelp(parser: OptionParser) { } class AttachmentContract : Contract { - override val legalContractReference: SecureHash - get() = SecureHash.zeroHash // TODO not implemented - override fun verify(tx: LedgerTransaction) { val state = tx.outputsOfType().single() val attachment = tx.attachments.single() diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 0b1ddf7076..f1485103f6 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -24,13 +24,15 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + // The bank of corda CorDapp depends upon Cash CorDapp features + cordapp project(':finance') + // Corda integration dependencies cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') cordaCompile project(':client:jfx') cordaCompile project(':client:rpc') - cordaCompile project(':finance') cordaCompile project(':webserver') cordaCompile project(':test-utils') @@ -54,7 +56,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] } node { name "CN=BankOfCorda,O=R3,L=New York,C=US" @@ -62,13 +64,13 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10005 rpcPort 10006 webPort 10007 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] rpcUsers = [ ['username' : "bankUser", 'password' : "test", - 'permissions': ["StartFlow.net.corda.flows.CashPaymentFlow", - "StartFlow.net.corda.flows.CashExitFlow", - "StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"]] + 'permissions': ["StartFlow.net.corda.finance.flows.CashPaymentFlow", + "StartFlow.net.corda.finance.flows.CashExitFlow", + "StartFlow.net.corda.finance.flows.CashIssueAndPaymentFlow"]] ] } node { @@ -77,7 +79,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10008 rpcPort 10009 webPort 10010 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] rpcUsers = [ ['username' : "bigCorpUser", 'password' : "test", @@ -87,7 +89,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } task integrationTest(type: Test, dependsOn: []) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 5baaa550e1..4df2768692 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -1,16 +1,15 @@ package net.corda.bank -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.DOLLARS +import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.startFlow -import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.Vault import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.getOrThrow -import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.node.services.startFlowPermission +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.* @@ -21,7 +20,8 @@ class BankOfCordaRPCClientTest { @Test fun `issuer flow via RPC`() { driver(dsl = { - val bocManager = User("bocManager", "password1", permissions = setOf(startFlowPermission())) + val bocManager = User("bocManager", "password1", permissions = setOf( + startFlowPermission())) val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet()) val (nodeBankOfCorda, nodeBigCorporation) = listOf( startNode(BOC.name, setOf(ServiceInfo(SimpleNotaryService.type)), listOf(bocManager)), @@ -38,21 +38,18 @@ class BankOfCordaRPCClientTest { // Register for Bank of Corda Vault updates val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) - val vaultUpdatesBoc = bocProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates + val vaultUpdatesBoc = bocProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates // Register for Big Corporation Vault updates - val vaultUpdatesBigCorp = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates + val vaultUpdatesBigCorp = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates // Kick-off actual Issuer Flow val anonymous = true - bocProxy.startFlow( - ::IssuanceRequester, - 1000.DOLLARS, + bocProxy.startFlow(::CashIssueAndPaymentFlow, + 1000.DOLLARS, BIG_CORP_PARTY_REF, nodeBigCorporation.nodeInfo.legalIdentity, - BIG_CORP_PARTY_REF, - nodeBankOfCorda.nodeInfo.legalIdentity, - nodeBankOfCorda.nodeInfo.notaryIdentity, - anonymous).returnValue.getOrThrow() + anonymous, + nodeBankOfCorda.nodeInfo.notaryIdentity) // Check Bank of Corda Vault Updates vaultUpdatesBoc.expectEvents { @@ -74,9 +71,9 @@ class BankOfCordaRPCClientTest { vaultUpdatesBigCorp.expectEvents { sequence( // MOVE - expect { update -> - require(update.consumed.isEmpty()) { update.consumed.size } - require(update.produced.size == 1) { update.produced.size } + expect { (consumed, produced) -> + require(consumed.isEmpty()) { consumed.size } + require(produced.size == 1) { produced.size } } ) } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index eda775af3f..0f61b602e0 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -5,12 +5,11 @@ import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType -import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort -import net.corda.flows.CashExitFlow -import net.corda.flows.CashPaymentFlow -import net.corda.flows.IssuerFlow -import net.corda.node.services.startFlowPermission +import net.corda.finance.flows.CashExitFlow +import net.corda.finance.flows.CashIssueAndPaymentFlow +import net.corda.finance.flows.CashPaymentFlow +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC @@ -67,10 +66,11 @@ private class BankOfCordaDriver { BANK_USERNAME, "test", permissions = setOf( - startFlowPermission(), - startFlowPermission(), + startFlowPermission(), startFlowPermission())) - val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission())) + val bigCorpUser = User(BIGCORP_USERNAME, "test", + permissions = setOf( + startFlowPermission())) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) val bankOfCorda = startNode( BOC.name, @@ -84,8 +84,7 @@ private class BankOfCordaDriver { Role.ISSUE_CASH_RPC -> { println("Requesting Cash via RPC ...") val result = BankOfCordaClientApi(NetworkHostAndPort("localhost", 10006)).requestRPCIssue(requestParams) - if (result is SignedTransaction) - println("Success!! You transaction receipt is ${result.tx.id}") + println("Success!! You transaction receipt is ${result.tx.id}") } Role.ISSUE_CASH_WEB -> { println("Requesting Cash via Web ...") diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index a2be1feb62..39222e7c2b 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -3,15 +3,14 @@ package net.corda.bank.api import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.client.rpc.CordaRPCClient import net.corda.core.contracts.Amount -import net.corda.core.contracts.currency import net.corda.core.messaging.startFlow -import net.corda.core.utilities.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.testing.DUMMY_NOTARY +import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.testing.http.HttpApi +import java.util.* /** * Interface for communicating with Bank of Corda node @@ -29,6 +28,8 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { /** * RPC API + * + * @return a pair of the issuing and payment transactions. */ fun requestRPCIssue(params: IssueRequestParams): SignedTransaction { val client = CordaRPCClient(hostAndPort) @@ -39,17 +40,15 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { // Resolve parties via RPC val issueToParty = rpc.partyFromX500Name(params.issueToPartyName) ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") - val issuerBankParty = rpc.partyFromX500Name(params.issuerBankName) - ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") val notaryLegalIdentity = rpc.partyFromX500Name(params.notaryName) ?: throw IllegalStateException("Unable to locate ${params.notaryName} in Network Map Service") val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity) ?: throw IllegalStateException("Unable to locate notary node in network map cache") - val amount = Amount(params.amount, currency(params.currency)) - val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) + val amount = Amount(params.amount, Currency.getInstance(params.currency)) + val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) - return rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryNode.notaryIdentity, params.anonymous) + return rpc.startFlow(::CashIssueAndPaymentFlow, amount, issuerBankPartyRef, issueToParty, params.anonymous, notaryNode.notaryIdentity) .returnValue.getOrThrow().stx } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 47e7c2bacc..c334013ce8 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -1,15 +1,15 @@ package net.corda.bank.api import net.corda.core.contracts.Amount -import net.corda.core.contracts.currency import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor -import net.corda.flows.IssuerFlow.IssuanceRequester +import net.corda.finance.flows.CashIssueAndPaymentFlow import org.bouncycastle.asn1.x500.X500Name import java.time.LocalDateTime +import java.util.* import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response @@ -18,7 +18,7 @@ import javax.ws.rs.core.Response @Path("bank") class BankOfCordaWebApi(val rpc: CordaRPCOps) { data class IssueRequestParams(val amount: Long, val currency: String, - val issueToPartyName: X500Name, val issueToPartyRefAsString: String, + val issueToPartyName: X500Name, val issuerBankPartyRef: String, val issuerBankName: X500Name, val notaryName: X500Name, val anonymous: Boolean) @@ -44,25 +44,23 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { // Resolve parties via RPC val issueToParty = rpc.partyFromX500Name(params.issueToPartyName) ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issueToPartyName} in identity service").build() - val issuerBankParty = rpc.partyFromX500Name(params.issuerBankName) - ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issuerBankName} in identity service").build() + rpc.partyFromX500Name(params.issuerBankName) ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.issuerBankName} in identity service").build() val notaryParty = rpc.partyFromX500Name(params.notaryName) ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${params.notaryName} in identity service").build() val notaryNode = rpc.nodeIdentityFromParty(notaryParty) - ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate ${notaryParty} in network map service").build() + ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate $notaryParty in network map service").build() - val amount = Amount(params.amount, currency(params.currency)) - val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) - val anonymous = params.anonymous + val amount = Amount(params.amount, Currency.getInstance(params.currency)) + val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) // invoke client side of Issuer Flow: IssuanceRequester // The line below blocks and waits for the future to resolve. return try { - rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow() - logger.info("Issue request completed successfully: $params") + rpc.startFlow(::CashIssueAndPaymentFlow, amount, issuerBankPartyRef, issueToParty, params.anonymous, notaryNode.notaryIdentity).returnValue.getOrThrow() + logger.info("Issue and payment request completed successfully: $params") Response.status(Response.Status.CREATED).build() } catch (e: Exception) { - logger.error("Issue request failed: ${e}", e) + logger.error("Issue and payment request failed", e) Response.status(Response.Status.FORBIDDEN).build() } } diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 70d42a9d19..e9f572b106 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -27,11 +27,13 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + // The irs demo CorDapp depends upon Cash CorDapp features + cordapp project(':finance') + // Corda integration dependencies cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') - cordaCompile project(':finance') cordaCompile project(':webserver') // Javax is required for webapis @@ -55,7 +57,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10002 rpcPort 10003 webPort 10004 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] useTestClock true } node { @@ -64,7 +66,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10005 rpcPort 10006 webPort 10007 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] useTestClock true } node { @@ -73,13 +75,13 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { p2pPort 10008 rpcPort 10009 webPort 10010 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] useTestClock true } } task integrationTest(type: Test, dependsOn: []) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index dc4c021c4b..1116623f2a 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -1,14 +1,15 @@ package net.corda.irs import net.corda.client.rpc.CordaRPCClient +import net.corda.core.internal.concurrent.transpose import net.corda.core.messaging.vaultTrackBy import net.corda.core.node.services.ServiceInfo import net.corda.core.toFuture -import net.corda.core.internal.concurrent.transpose import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.loggerFor import net.corda.core.utilities.seconds +import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.irs.api.NodeInterestRates import net.corda.irs.contract.InterestRateSwap import net.corda.irs.utilities.uploadFile @@ -61,6 +62,7 @@ class IRSDemoTest : IntegrationTestCategory { val (_, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map { val mapper = net.corda.jackson.JacksonSupport.createDefaultMapper(it.first.rpc) + registerFinanceJSONMappers(mapper) HttpApi.fromHostAndPort(it.second, "api/irs", mapper = mapper) } val nextFixingDates = getFixingDateObservable(nodeA.configuration) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index 76e6c6007c..f9ec94b819 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -1,13 +1,6 @@ package net.corda.irs.api import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.BusinessCalendar -import net.corda.contracts.Fix -import net.corda.contracts.FixOf -import net.corda.contracts.Tenor -import net.corda.contracts.math.CubicSplineInterpolator -import net.corda.contracts.math.Interpolator -import net.corda.contracts.math.InterpolatorFactory import net.corda.core.contracts.Command import net.corda.core.crypto.* import net.corda.core.flows.FlowException @@ -24,6 +17,13 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap +import net.corda.finance.contracts.BusinessCalendar +import net.corda.finance.contracts.Fix +import net.corda.finance.contracts.FixOf +import net.corda.finance.contracts.Tenor +import net.corda.finance.contracts.math.CubicSplineInterpolator +import net.corda.finance.contracts.math.Interpolator +import net.corda.finance.contracts.math.InterpolatorFactory import net.corda.irs.flows.RatesFixFlow import org.apache.commons.io.IOUtils import java.math.BigDecimal diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index 8c032f695d..aa901cfe6b 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -1,9 +1,7 @@ package net.corda.irs.contract import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import net.corda.contracts.* import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty @@ -12,6 +10,7 @@ import net.corda.core.node.services.ServiceType import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.* import net.corda.irs.api.NodeInterestRates import net.corda.irs.flows.FixingFlow import net.corda.irs.utilities.suggestInterestRateAnnouncementTimeWindow @@ -193,8 +192,6 @@ class FloatingRatePaymentEvent(date: LocalDate, * This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model. */ class InterestRateSwap : Contract { - override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") - /** * This Common area contains all the information that is not leg specific. */ diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index 1401fbc41e..12ef6e2cd4 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -1,10 +1,10 @@ package net.corda.irs.contract -import net.corda.contracts.Tenor import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import net.corda.core.contracts.Amount import net.corda.core.serialization.CordaSerializable +import net.corda.finance.contracts.Tenor import java.math.BigDecimal import java.util.* diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt index f11d5c25b1..099d82c73a 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt @@ -1,7 +1,6 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.DealState import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow @@ -10,10 +9,11 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker -import net.corda.flows.TwoPartyDealFlow -import net.corda.flows.TwoPartyDealFlow.Acceptor -import net.corda.flows.TwoPartyDealFlow.AutoOffer -import net.corda.flows.TwoPartyDealFlow.Instigator +import net.corda.finance.contracts.DealState +import net.corda.finance.flows.TwoPartyDealFlow +import net.corda.finance.flows.TwoPartyDealFlow.Acceptor +import net.corda.finance.flows.TwoPartyDealFlow.AutoOffer +import net.corda.finance.flows.TwoPartyDealFlow.Instigator /** * This whole class is really part of a demo just to initiate the agreement of a deal with a simple diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index a902252889..65b6bb6ad7 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -1,8 +1,6 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.Fix -import net.corda.contracts.FixableDealState import net.corda.core.contracts.* import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.toBase58String @@ -20,7 +18,9 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.seconds import net.corda.core.utilities.trace import net.corda.core.utilities.transient -import net.corda.flows.TwoPartyDealFlow +import net.corda.finance.contracts.Fix +import net.corda.finance.contracts.FixableDealState +import net.corda.finance.flows.TwoPartyDealFlow import java.math.BigDecimal import java.security.PublicKey diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index dde01b1226..9276444df7 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -1,8 +1,6 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.Fix -import net.corda.contracts.FixOf import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.FlowLogic @@ -13,6 +11,8 @@ import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap +import net.corda.finance.contracts.Fix +import net.corda.finance.contracts.FixOf import net.corda.irs.flows.RatesFixFlow.FixOutOfRange import java.math.BigDecimal import java.util.* diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt index 80de4531b1..38d5455e8f 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt @@ -1,5 +1,7 @@ package net.corda.irs.plugin +import com.fasterxml.jackson.databind.ObjectMapper +import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.irs.api.InterestRateSwapAPI import net.corda.webserver.services.WebServerPluginRegistry import java.util.function.Function @@ -9,4 +11,6 @@ class IRSPlugin : WebServerPluginRegistry { override val staticServeDirs: Map = mapOf( "irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm() ) + + override fun customizeJSONSerialization(om: ObjectMapper): Unit = registerFinanceJSONMappers(om) } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index c501759e09..2d9e0577ef 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -1,12 +1,9 @@ package net.corda.irs.api -import net.corda.contracts.Fix -import net.corda.contracts.FixOf -import net.corda.contracts.asset.CASH -import net.corda.contracts.asset.Cash -import net.corda.contracts.asset.`issued by` -import net.corda.contracts.asset.`owned by` -import net.corda.core.contracts.* +import net.corda.core.contracts.Command +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.`with notary` import net.corda.core.crypto.MerkleTreeException import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.Party @@ -14,6 +11,13 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.Fix +import net.corda.finance.contracts.FixOf +import net.corda.finance.contracts.asset.CASH +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.contracts.asset.`issued by` +import net.corda.finance.contracts.asset.`owned by` import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -57,7 +61,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Before fun setUp() { - database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), identitySvc = ::makeTestIdentityService) + database = configureDatabase(makeTestDataSourceProperties(), makeTestDatabaseProperties(), createIdentityService = ::makeTestIdentityService) database.transaction { oracle = NodeInterestRates.Oracle( MEGA_CORP, @@ -122,7 +126,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `refuse to sign with no relevant commands`() { database.transaction { - val tx = makeTX() + val tx = makeFullTx() val wtx1 = tx.toWireTransaction() fun filterAllOutputs(elem: Any): Boolean { return when (elem) { @@ -144,7 +148,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `sign successfully`() { database.transaction { - val tx = makeTX() + val tx = makePartialTX() val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first() tx.addCommand(fix, oracle.identity.owningKey) // Sign successfully. @@ -158,7 +162,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `do not sign with unknown fix`() { database.transaction { - val tx = makeTX() + val tx = makePartialTX() val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val badFix = Fix(fixOf, BigDecimal("0.6789")) tx.addCommand(badFix, oracle.identity.owningKey) @@ -172,7 +176,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `do not sign too many leaves`() { database.transaction { - val tx = makeTX() + val tx = makePartialTX() val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first() fun filtering(elem: Any): Boolean { return when (elem) { @@ -190,7 +194,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { @Test fun `empty partial transaction to sign`() { - val tx = makeTX() + val tx = makeFullTx() val wtx = tx.toWireTransaction() val ftx = wtx.buildFilteredTransaction(Predicate { false }) assertFailsWith { oracle.sign(ftx) } @@ -206,7 +210,7 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { n2.database.transaction { n2.installCordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA } - val tx = TransactionBuilder(null) + val tx = makePartialTX() val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val oracle = n2.info.serviceIdentities(NodeInterestRates.Oracle.type).first() val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1")) @@ -237,6 +241,8 @@ class NodeInterestRatesTest : TestDependencyInjectionBase() { } } - private fun makeTX() = TransactionBuilder(DUMMY_NOTARY).withItems( + private fun makePartialTX() = TransactionBuilder(DUMMY_NOTARY).withItems( 1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `with notary` DUMMY_NOTARY) + + private fun makeFullTx() = makePartialTX().withItems(dummyCommand()) } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 68c4bb195b..338f723668 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -1,10 +1,13 @@ package net.corda.irs.contract -import net.corda.contracts.* -import net.corda.core.contracts.* +import net.corda.core.contracts.Amount +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.seconds +import net.corda.finance.DOLLARS +import net.corda.finance.EUR +import net.corda.finance.contracts.* import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Test @@ -79,8 +82,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { fixedLegPaymentSchedule = mutableMapOf() ) - val EUR = currency("EUR") - val common = InterestRateSwap.Common( baseCurrency = EUR, eligibleCurrency = EUR, @@ -169,8 +170,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { fixedLegPaymentSchedule = mutableMapOf() ) - val EUR = currency("EUR") - val common = InterestRateSwap.Common( baseCurrency = EUR, eligibleCurrency = EUR, diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index d229f24cc7..c18dac37b2 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -9,14 +9,15 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party -import net.corda.core.internal.concurrent.* import net.corda.core.internal.FlowStateMachine +import net.corda.core.internal.concurrent.* import net.corda.core.node.services.queryBy import net.corda.core.toFuture import net.corda.core.transactions.SignedTransaction -import net.corda.flows.TwoPartyDealFlow.Acceptor -import net.corda.flows.TwoPartyDealFlow.AutoOffer -import net.corda.flows.TwoPartyDealFlow.Instigator +import net.corda.finance.flows.TwoPartyDealFlow.Acceptor +import net.corda.finance.flows.TwoPartyDealFlow.AutoOffer +import net.corda.finance.flows.TwoPartyDealFlow.Instigator +import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.irs.contract.InterestRateSwap import net.corda.irs.flows.FixingFlow import net.corda.jackson.JacksonSupport @@ -42,8 +43,14 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>()) override fun startMainSimulation(): CordaFuture { + // TODO: Determine why this isn't happening via the network map + mockNet.nodes.map { it.services.identityService }.forEach { service -> + mockNet.nodes.forEach { node -> service.registerIdentity(node.info.legalIdentityAndCert) } + } + val future = openFuture() om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate)) + registerFinanceJSONMappers(om) startIRSDealBetween(0, 1).thenMatch({ // Next iteration is a pause. diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index 567bfe03f5..46d1bf0ae6 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -6,7 +6,7 @@ import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY import net.corda.demorun.runNodes -import net.corda.node.services.startFlowPermission +import net.corda.node.services.FlowPermissions.Companion.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User import net.corda.notarydemo.flows.DummyIssueAndMove diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index 4db198241d..dd7731e872 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -1,9 +1,10 @@ package net.corda.notarydemo.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.contracts.asset.Cash -import net.corda.core.contracts.* -import net.corda.core.crypto.sha256 +import net.corda.core.contracts.Amount +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.Issued import net.corda.core.flows.FlowLogic import net.corda.core.flows.StartableByRPC import net.corda.core.identity.AbstractParty @@ -11,11 +12,12 @@ import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.GBP +import net.corda.finance.contracts.asset.Cash @StartableByRPC class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: Party, private val discriminator: Int) : FlowLogic() { object DoNothingContract : Contract { - override val legalContractReference = byteArrayOf().sha256() override fun verify(tx: LedgerTransaction) {} } diff --git a/samples/simm-valuation-demo/README.md b/samples/simm-valuation-demo/README.md index ef0c08883b..7ea71fe116 100644 --- a/samples/simm-valuation-demo/README.md +++ b/samples/simm-valuation-demo/README.md @@ -19,8 +19,7 @@ This demo was built in partnership with OpenGamma and used their SIMM library. H | Could not find net.corda.(...):(...):0.6-SNAPSHOT | The corda libraries have not been installed into your local maven directory. View the instructions for doing this in the core corda repository | | Execution failed for task ':simm-valuation-demo:buildWeb' : A problem occurred starting process 'command 'ng'' | You need to have `node packet manager` installed in order to build out some of the web resources. This is not a necessary step as we include pre-built web resources but if you do modify the web source, you will need to rebuild this area | - - - - +## Rebuild the web resources +* Get Node.js v6.11.2 which at time of writing is the LTS release +* ../../gradlew installWeb diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 41deacb868..7c39385cb5 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -1,3 +1,5 @@ +import org.apache.tools.ant.filters.FixCrLfFilter + buildscript { ext.strata_version = '1.1.2' } @@ -28,11 +30,13 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + // The SIMM demo CorDapp depends upon Cash CorDapp features + cordapp project(':finance') + // Corda integration dependencies cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts') cordaCompile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') cordaCompile project(':core') - cordaCompile project(':finance') cordaCompile project(':webserver') // Javax is required for webapis @@ -64,33 +68,33 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { name "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" advertisedServices = ["corda.notary.validating"] p2pPort 10002 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] } node { name "CN=Bank A,O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10004 webPort 10005 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] } node { name "CN=Bank B,O=Bank B,L=New York,C=US" advertisedServices = [] p2pPort 10006 webPort 10007 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] } node { name "CN=Bank C,O=Bank C,L=Tokyo,C=Japan" advertisedServices = [] p2pPort 10008 webPort 10009 - cordapps = [] + cordapps = ["net.corda:finance:$corda_release_version"] } } task integrationTest(type: Test, dependsOn: []) { - testClassesDir = sourceSets.integrationTest.output.classesDir + testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath } @@ -117,13 +121,18 @@ task buildWeb(type: Exec, dependsOn: [cleanWeb, npmInstall]) { if (System.getProperty('os.name').toLowerCase().contains('windows')) { commandLine 'cmd', '/c', 'ng', 'build' } else { - commandLine 'ng', 'build' + commandLine 'node_modules/angular-cli/bin/ng', 'build' } } task installWeb(type: Copy, dependsOn: [buildWeb]) { from 'src/main/web/dist' into 'src/main/resources/simmvaluationweb' + ['**/*.js', '**/*.js.map', '**/*.ts'].each { + filesMatching(it) { + filter(FixCrLfFilter.class, eol: FixCrLfFilter.CrLf.LF, fixlast: false) + } + } } publishing { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt index 2dcb81f3fb..66dd387c22 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt @@ -1,8 +1,6 @@ package net.corda.vega.api import com.opengamma.strata.basics.currency.MultiCurrencyAmount -import net.corda.client.rpc.notUsed -import net.corda.contracts.DealState import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.parsePublicKeyBase58 import net.corda.core.crypto.toBase58String @@ -13,6 +11,7 @@ import net.corda.core.messaging.startFlow import net.corda.core.messaging.vaultQueryBy import net.corda.core.node.services.ServiceType import net.corda.core.utilities.getOrThrow +import net.corda.finance.contracts.DealState import net.corda.vega.analytics.InitialMarginTriple import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.PortfolioState diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt index cda90b40d4..fb13e86d76 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt @@ -10,7 +10,6 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.crypto.toBase58String import net.corda.core.messaging.CordaRPCOps -import net.corda.core.node.ServiceHub import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.PortfolioState import net.corda.vega.portfolio.Portfolio @@ -169,7 +168,6 @@ class PortfolioApiUtils(private val ownParty: Party) { ), common = mapOf( "valuationDate" to trade.product.startDate.unadjusted, - "hashLegalDocs" to state.contract.legalContractReference.toString(), "interestRate" to mapOf( "name" to "TODO", "oracle" to "TODO", diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt index 8d40347f27..a7b02c55a0 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt @@ -1,12 +1,12 @@ package net.corda.vega.contracts -import net.corda.contracts.DealState import net.corda.core.contracts.Command import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.keys import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.DealState import java.security.PublicKey /** diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt index 1e7b788be6..420b66dd7b 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt @@ -1,14 +1,13 @@ package net.corda.vega.contracts import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.transactions.LedgerTransaction import java.math.BigDecimal /** * Specifies the contract between two parties that trade an OpenGamma IRS. Currently can only agree to trade. */ -data class OGTrade(override val legalContractReference: SecureHash = SecureHash.sha256("OGTRADE.KT")) : Contract { +class OGTrade : Contract { override fun verify(tx: LedgerTransaction) { requireNotNull(tx.timeWindow) { "must have a time-window" } val groups: List> = tx.groupStates { state -> state.linearId } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt index 5fb811f26e..ee351737c5 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt @@ -1,6 +1,5 @@ package net.corda.vega.contracts -import net.corda.contracts.DealState import net.corda.core.contracts.* import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogicRefFactory @@ -8,6 +7,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder +import net.corda.finance.contracts.DealState import net.corda.vega.flows.SimmRevaluation import java.security.PublicKey import java.time.LocalDate diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt index 38aa7a8acb..e0c3ba4b9a 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt @@ -1,7 +1,6 @@ package net.corda.vega.contracts import net.corda.core.contracts.* -import net.corda.core.crypto.SecureHash import net.corda.core.transactions.LedgerTransaction /** @@ -9,7 +8,7 @@ import net.corda.core.transactions.LedgerTransaction * Implements an agree clause to agree to the portfolio and an update clause to change either the portfolio or valuation * of the portfolio arbitrarily. */ -data class PortfolioSwap(override val legalContractReference: SecureHash = SecureHash.sha256("swordfish")) : Contract { +data class PortfolioSwap(private val blank: Void? = null) : Contract { override fun verify(tx: LedgerTransaction) { requireNotNull(tx.timeWindow) { "must have a time-window)" } val groups: List> = tx.groupStates { state -> state.linearId } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt index ec39e261a0..ad8c949331 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt @@ -9,7 +9,7 @@ import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap -import net.corda.flows.TwoPartyDealFlow +import net.corda.finance.flows.TwoPartyDealFlow import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.OGTrade import net.corda.vega.contracts.SwapData diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index 9fb4a1ab30..31acaa837d 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -19,7 +19,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap -import net.corda.flows.TwoPartyDealFlow +import net.corda.finance.flows.TwoPartyDealFlow import net.corda.vega.analytics.* import net.corda.vega.contracts.* import net.corda.vega.portfolio.Portfolio diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt index 3eda5bef8e..3b1f492b6c 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/plugin/SimmPlugin.kt @@ -1,6 +1,8 @@ package net.corda.vega.plugin +import com.fasterxml.jackson.databind.ObjectMapper import net.corda.core.node.CordaPluginRegistry +import net.corda.finance.plugin.registerFinanceJSONMappers import net.corda.vega.api.PortfolioApi import net.corda.webserver.services.WebServerPluginRegistry import java.util.function.Function @@ -14,4 +16,5 @@ import java.util.function.Function class SimmPlugin : WebServerPluginRegistry { override val webApis = listOf(Function(::PortfolioApi)) override val staticServeDirs: Map = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm()) + override fun customizeJSONSerialization(om: ObjectMapper): Unit = registerFinanceJSONMappers(om) } \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map index c0424984e9..da51d02cc7 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/Deal.js.map @@ -1 +1 @@ -{"version":3,"file":"Deal.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/Deal.ts"],"names":[],"mappings":";AAGA,8BAA8B,uBAAuB,CAAC,CAAA;AACtD,iCAAiC,0BAA0B,CAAC,CAAA;AAC5D,4BAA4B,qBAAqB,CAAC,CAAA;AAIlD,IAAY,CAAC,WAAM,YAAY,CAAC,CAAA;AAEhC,IAAI,gBAAgB,GAAG;IACrB,UAAU,EAAE,gMAAgM;IAC5M,0BAA0B,EAAE,EAE3B;IACD,uBAAuB,EAAE,EAExB;CACF,CAAC;AAEF,IAAI,cAAc,GAAG;IACnB,SAAS,EAAE;QACT,KAAK,EAAE,IAAI,CAAC,IAAI;KACjB;CACF,CAAC;AAEF,IAAI,WAAW,GAAG;IAChB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,SAAS;CACjB,CAAC;AAEF,IAAI,cAAc,GAAG;IACnB,KAAK,EAAE,QAAQ;IACf,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,QAAQ;CAChB,CAAC;AAEF,IAAI,GAAG,GAAG;IACR,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;AACpB,CAAC,CAAA;AAED,wEAAwE;AACxE,IAAI,UAAU,GAAG,UAAC,CAAC,EAAE,CAAC;IACpB,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1B,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAED;IAGE,cAAoB,aAA4B,EAAU,WAAwB,EAAU,UAAsB;QAHpH,iBAwEC;QArEqB,kBAAa,GAAb,aAAa,CAAe;QAAU,gBAAW,GAAX,WAAW,CAAa;QAAU,eAAU,GAAV,UAAU,CAAY;QAFlH,YAAO,GAAG,MAAI,GAAG,EAAE,CAAC,cAAc,EAAE,SAAI,GAAG,EAAE,CAAC,WAAW,EAAE,SAAI,GAAG,EAAE,CAAC,UAAU,EAAE,SAAI,GAAG,EAAE,CAAC,WAAW,EAAE,SAAI,GAAG,EAAE,CAAC,aAAa,EAAE,SAAI,GAAG,EAAE,CAAC,aAAa,EAAE,SAAI,GAAG,EAAE,CAAC,kBAAkB,EAAI,CAAA;QAI1L,oBAAe,GAAG,UAAC,UAA6B,EAAE,QAAyB;YACzE,IAAI,QAAQ,GAAG,IAAI,6BAAa,EAAE,CAAC;YAEnC,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAEjC,QAAQ,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAChD,QAAQ,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;YAChD,QAAQ,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;YACpD,QAAQ,CAAC,SAAS,GAAG,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,GAAG,EAAE,EAAE,CAAC;YAClF,QAAQ,CAAC,gBAAgB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC;YAC9F,QAAQ,CAAC,iBAAiB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC;YAChG,QAAQ,CAAC,eAAe,GAAG,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YAEjE,MAAM,CAAC,QAAQ,CAAC;QAClB,CAAC,CAAC;QAEF,uBAAkB,GAAG,UAAC,aAAmC,EAAE,QAAyB;YAClF,IAAI,WAAW,GAAG,IAAI,mCAAgB,EAAE,CAAC;YAEzC,UAAU,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;YAEvC,WAAW,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YACnD,WAAW,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;YACnD,WAAW,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;YACvD,WAAW,CAAC,gBAAgB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC;YACpG,WAAW,CAAC,iBAAiB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC;YACtG,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YACvD,WAAW,CAAC,cAAc,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YACrE,WAAW,CAAC,eAAe,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YAEtE,MAAM,CAAC,WAAW,CAAC;QACrB,CAAC,CAAC;QAEF,kBAAa,GAAG,UAAC,QAAyB;YACxC,IAAI,MAAM,GAAG,IAAI,yBAAW,EAAE,CAAC;YAE/B,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAE7B,MAAM,CAAC,OAAO,GAAG,KAAI,CAAC,OAAO,CAAC;YAC9B,MAAM,CAAC,gBAAgB,GAAG,QAAQ,CAAC,YAAY,CAAC;YAChD,MAAM,CAAC,kBAAkB,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YACxD,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC/C,MAAM,CAAC,qBAAqB,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC3D,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAE9C,MAAM,CAAC,MAAM,CAAC;QAChB,CAAC,CAAC;QAEF,WAAM,GAAG;YACP,IAAI,QAAQ,GAAG,KAAI,CAAC,aAAa,CAAC,MAAM,CAAC;YACzC,IAAI,aAAa,GAAG,KAAI,CAAC,aAAa,CAAC,WAAW,CAAC;YACnD,IAAI,UAAU,GAAG,KAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;YAE7C,IAAI,QAAQ,GAAG,KAAI,CAAC,eAAe,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YAC1D,IAAI,WAAW,GAAG,KAAI,CAAC,kBAAkB,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;YACnE,IAAI,MAAM,GAAG,KAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC1C,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;YAE7C,IAAI,IAAI,GAAG;gBACT,QAAQ,EAAE,QAAQ;gBAClB,WAAW,EAAE,WAAW;gBACxB,WAAW,EAAE,gBAAgB;gBAC7B,MAAM,EAAE,MAAM;aACf,CAAA;YAED,MAAM,CAAC,IAAI,CAAC;QACd,CAAC,CAAC;IApEmH,CAAC;IAqExH,WAAC;AAAD,CAAC,AAxED,IAwEC;AAxEY,YAAI,OAwEhB,CAAA;AAAA,CAAC","sourcesContent":["import { DealViewModel } from './viewmodel/DealViewModel';\r\nimport { NodeService } from './node.service';\r\nimport { IRSService } from './irs.service';\r\nimport { FixedLegModel } from './model/FixedLegModel';\r\nimport { FloatingLegModel } from './model/FloatingLegModel';\r\nimport { CommonModel } from './model/CommonModel';\r\nimport { FixedLegViewModel } from './viewmodel/FixedLegViewModel';\r\nimport { FloatingLegViewModel } from './viewmodel/FloatingLegViewModel';\r\nimport { CommonViewModel } from './viewmodel/CommonViewModel';\r\nimport * as _ from 'underscore';\r\n\r\nlet calculationModel = {\r\n expression: \"( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))\",\r\n floatingLegPaymentSchedule: {\r\n\r\n },\r\n fixedLegPaymentSchedule: {\r\n\r\n }\r\n};\r\n\r\nlet fixedRateModel = {\r\n ratioUnit: {\r\n value: 0.01 // %\r\n }\r\n};\r\n\r\nlet indexLookup = {\r\n \"GBP\": \"ICE LIBOR\",\r\n \"USD\": \"ICE LIBOR\",\r\n \"EUR\": \"EURIBOR\"\r\n};\r\n\r\nlet calendarLookup = {\r\n \"GBP\": \"London\",\r\n \"USD\": \"NewYork\",\r\n \"EUR\": \"London\"\r\n};\r\n\r\nlet now = () => {\r\n return new Date();\r\n}\r\n\r\n// Copy the value of the field from b to a if it exists on both objects.\r\nlet unionMerge = (a, b) => {\r\n for (let key in b) {\r\n if (a.hasOwnProperty(key)) {\r\n a[key] = b[key];\r\n }\r\n }\r\n}\r\n\r\nexport class Deal {\r\n tradeId = `T${now().getUTCFullYear()}-${now().getUTCMonth()}-${now().getUTCDate()}.${now().getUTCHours()}:${now().getUTCMinutes()}:${now().getUTCSeconds()}:${now().getUTCMilliseconds()}`\r\n\r\n constructor(private dealViewModel: DealViewModel, private nodeService: NodeService, private irsService: IRSService) {}\r\n\r\n toFixedLegModel = (fixedLegVM: FixedLegViewModel, commonVM: CommonViewModel) => {\r\n let fixedLeg = new FixedLegModel();\r\n\r\n unionMerge(fixedLeg, fixedLegVM);\r\n\r\n fixedLeg.notional.token = commonVM.baseCurrency;\r\n fixedLeg.effectiveDate = commonVM.effectiveDate;\r\n fixedLeg.terminationDate = commonVM.terminationDate;\r\n fixedLeg.fixedRate = { ratioUnit: { value: Number(fixedLegVM.fixedRate) / 100 } };\r\n fixedLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).day;\r\n fixedLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).year;\r\n fixedLeg.paymentCalendar = calendarLookup[commonVM.baseCurrency];\r\n\r\n return fixedLeg;\r\n };\r\n\r\n toFloatingLegModel = (floatingLegVM: FloatingLegViewModel, commonVM: CommonViewModel) => {\r\n let floatingLeg = new FloatingLegModel();\r\n\r\n unionMerge(floatingLeg, floatingLegVM);\r\n\r\n floatingLeg.notional.token = commonVM.baseCurrency;\r\n floatingLeg.effectiveDate = commonVM.effectiveDate;\r\n floatingLeg.terminationDate = commonVM.terminationDate;\r\n floatingLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).day;\r\n floatingLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).year;\r\n floatingLeg.index = indexLookup[commonVM.baseCurrency];\r\n floatingLeg.fixingCalendar = [calendarLookup[commonVM.baseCurrency]];\r\n floatingLeg.paymentCalendar = [calendarLookup[commonVM.baseCurrency]];\r\n\r\n return floatingLeg;\r\n };\r\n\r\n toCommonModel = (commonVM: CommonViewModel) => {\r\n let common = new CommonModel();\r\n\r\n unionMerge(common, commonVM);\r\n\r\n common.tradeID = this.tradeId;\r\n common.eligibleCurrency = commonVM.baseCurrency;\r\n common.independentAmounts.token = commonVM.baseCurrency;\r\n common.threshold.token = commonVM.baseCurrency;\r\n common.minimumTransferAmount.token = commonVM.baseCurrency;\r\n common.rounding.token = commonVM.baseCurrency;\r\n\r\n return common;\r\n };\r\n\r\n toJson = () => {\r\n let commonVM = this.dealViewModel.common;\r\n let floatingLegVM = this.dealViewModel.floatingLeg;\r\n let fixedLegVM = this.dealViewModel.fixedLeg;\r\n\r\n let fixedLeg = this.toFixedLegModel(fixedLegVM, commonVM);\r\n let floatingLeg = this.toFloatingLegModel(floatingLegVM, commonVM);\r\n let common = this.toCommonModel(commonVM);\r\n _.assign(fixedLeg.fixedRate, fixedRateModel);\r\n\r\n let json = {\r\n fixedLeg: fixedLeg,\r\n floatingLeg: floatingLeg,\r\n calculation: calculationModel,\r\n common: common\r\n }\r\n\r\n return json;\r\n };\r\n};\r\n"]} \ No newline at end of file +{"version":3,"file":"Deal.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/Deal.ts"],"names":[],"mappings":";AAGA,8BAA8B,uBAAuB,CAAC,CAAA;AACtD,iCAAiC,0BAA0B,CAAC,CAAA;AAC5D,4BAA4B,qBAAqB,CAAC,CAAA;AAIlD,IAAY,CAAC,WAAM,YAAY,CAAC,CAAA;AAEhC,IAAI,gBAAgB,GAAG;IACrB,UAAU,EAAE,gMAAgM;IAC5M,0BAA0B,EAAE,EAE3B;IACD,uBAAuB,EAAE,EAExB;CACF,CAAC;AAEF,IAAI,cAAc,GAAG;IACnB,SAAS,EAAE;QACT,KAAK,EAAE,IAAI,CAAC,IAAI;KACjB;CACF,CAAC;AAEF,IAAI,WAAW,GAAG;IAChB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,SAAS;CACjB,CAAC;AAEF,IAAI,cAAc,GAAG;IACnB,KAAK,EAAE,QAAQ;IACf,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,QAAQ;CAChB,CAAC;AAEF,IAAI,GAAG,GAAG;IACR,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;AACpB,CAAC,CAAA;AAED,wEAAwE;AACxE,IAAI,UAAU,GAAG,UAAC,CAAC,EAAE,CAAC;IACpB,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1B,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;AACH,CAAC,CAAA;AAED;IAGE,cAAoB,aAA4B,EAAU,WAAwB,EAAU,UAAsB;QAHpH,iBAwEC;QArEqB,kBAAa,GAAb,aAAa,CAAe;QAAU,gBAAW,GAAX,WAAW,CAAa;QAAU,eAAU,GAAV,UAAU,CAAY;QAFlH,YAAO,GAAG,MAAI,GAAG,EAAE,CAAC,cAAc,EAAE,SAAI,GAAG,EAAE,CAAC,WAAW,EAAE,SAAI,GAAG,EAAE,CAAC,UAAU,EAAE,SAAI,GAAG,EAAE,CAAC,WAAW,EAAE,SAAI,GAAG,EAAE,CAAC,aAAa,EAAE,SAAI,GAAG,EAAE,CAAC,aAAa,EAAE,SAAI,GAAG,EAAE,CAAC,kBAAkB,EAAI,CAAA;QAI1L,oBAAe,GAAG,UAAC,UAA6B,EAAE,QAAyB;YACzE,IAAI,QAAQ,GAAG,IAAI,6BAAa,EAAE,CAAC;YAEnC,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAEjC,QAAQ,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAChD,QAAQ,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;YAChD,QAAQ,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;YACpD,QAAQ,CAAC,SAAS,GAAG,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,GAAG,EAAE,EAAE,CAAC;YAClF,QAAQ,CAAC,gBAAgB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC;YAC9F,QAAQ,CAAC,iBAAiB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC;YAChG,QAAQ,CAAC,eAAe,GAAG,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YAEjE,MAAM,CAAC,QAAQ,CAAC;QAClB,CAAC,CAAC;QAEF,uBAAkB,GAAG,UAAC,aAAmC,EAAE,QAAyB;YAClF,IAAI,WAAW,GAAG,IAAI,mCAAgB,EAAE,CAAC;YAEzC,UAAU,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;YAEvC,WAAW,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YACnD,WAAW,CAAC,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC;YACnD,WAAW,CAAC,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC;YACvD,WAAW,CAAC,gBAAgB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC;YACpG,WAAW,CAAC,iBAAiB,GAAG,KAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC;YACtG,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YACvD,WAAW,CAAC,cAAc,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YACrE,WAAW,CAAC,eAAe,GAAG,CAAC,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;YAEtE,MAAM,CAAC,WAAW,CAAC;QACrB,CAAC,CAAC;QAEF,kBAAa,GAAG,UAAC,QAAyB;YACxC,IAAI,MAAM,GAAG,IAAI,yBAAW,EAAE,CAAC;YAE/B,UAAU,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAE7B,MAAM,CAAC,OAAO,GAAG,KAAI,CAAC,OAAO,CAAC;YAC9B,MAAM,CAAC,gBAAgB,GAAG,QAAQ,CAAC,YAAY,CAAC;YAChD,MAAM,CAAC,kBAAkB,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YACxD,MAAM,CAAC,SAAS,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC/C,MAAM,CAAC,qBAAqB,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC3D,MAAM,CAAC,QAAQ,CAAC,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC;YAE9C,MAAM,CAAC,MAAM,CAAC;QAChB,CAAC,CAAC;QAEF,WAAM,GAAG;YACP,IAAI,QAAQ,GAAG,KAAI,CAAC,aAAa,CAAC,MAAM,CAAC;YACzC,IAAI,aAAa,GAAG,KAAI,CAAC,aAAa,CAAC,WAAW,CAAC;YACnD,IAAI,UAAU,GAAG,KAAI,CAAC,aAAa,CAAC,QAAQ,CAAC;YAE7C,IAAI,QAAQ,GAAG,KAAI,CAAC,eAAe,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YAC1D,IAAI,WAAW,GAAG,KAAI,CAAC,kBAAkB,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;YACnE,IAAI,MAAM,GAAG,KAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;YAC1C,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;YAE7C,IAAI,IAAI,GAAG;gBACT,QAAQ,EAAE,QAAQ;gBAClB,WAAW,EAAE,WAAW;gBACxB,WAAW,EAAE,gBAAgB;gBAC7B,MAAM,EAAE,MAAM;aACf,CAAA;YAED,MAAM,CAAC,IAAI,CAAC;QACd,CAAC,CAAC;IApEmH,CAAC;IAqExH,WAAC;AAAD,CAAC,AAxED,IAwEC;AAxEY,YAAI,OAwEhB,CAAA;AAAA,CAAC","sourcesContent":["import { DealViewModel } from './viewmodel/DealViewModel';\nimport { NodeService } from './node.service';\nimport { IRSService } from './irs.service';\nimport { FixedLegModel } from './model/FixedLegModel';\nimport { FloatingLegModel } from './model/FloatingLegModel';\nimport { CommonModel } from './model/CommonModel';\nimport { FixedLegViewModel } from './viewmodel/FixedLegViewModel';\nimport { FloatingLegViewModel } from './viewmodel/FloatingLegViewModel';\nimport { CommonViewModel } from './viewmodel/CommonViewModel';\nimport * as _ from 'underscore';\n\nlet calculationModel = {\n expression: \"( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))\",\n floatingLegPaymentSchedule: {\n\n },\n fixedLegPaymentSchedule: {\n\n }\n};\n\nlet fixedRateModel = {\n ratioUnit: {\n value: 0.01 // %\n }\n};\n\nlet indexLookup = {\n \"GBP\": \"ICE LIBOR\",\n \"USD\": \"ICE LIBOR\",\n \"EUR\": \"EURIBOR\"\n};\n\nlet calendarLookup = {\n \"GBP\": \"London\",\n \"USD\": \"NewYork\",\n \"EUR\": \"London\"\n};\n\nlet now = () => {\n return new Date();\n}\n\n// Copy the value of the field from b to a if it exists on both objects.\nlet unionMerge = (a, b) => {\n for (let key in b) {\n if (a.hasOwnProperty(key)) {\n a[key] = b[key];\n }\n }\n}\n\nexport class Deal {\n tradeId = `T${now().getUTCFullYear()}-${now().getUTCMonth()}-${now().getUTCDate()}.${now().getUTCHours()}:${now().getUTCMinutes()}:${now().getUTCSeconds()}:${now().getUTCMilliseconds()}`\n\n constructor(private dealViewModel: DealViewModel, private nodeService: NodeService, private irsService: IRSService) {}\n\n toFixedLegModel = (fixedLegVM: FixedLegViewModel, commonVM: CommonViewModel) => {\n let fixedLeg = new FixedLegModel();\n\n unionMerge(fixedLeg, fixedLegVM);\n\n fixedLeg.notional.token = commonVM.baseCurrency;\n fixedLeg.effectiveDate = commonVM.effectiveDate;\n fixedLeg.terminationDate = commonVM.terminationDate;\n fixedLeg.fixedRate = { ratioUnit: { value: Number(fixedLegVM.fixedRate) / 100 } };\n fixedLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).day;\n fixedLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(fixedLegVM.dayCountBasis).year;\n fixedLeg.paymentCalendar = calendarLookup[commonVM.baseCurrency];\n\n return fixedLeg;\n };\n\n toFloatingLegModel = (floatingLegVM: FloatingLegViewModel, commonVM: CommonViewModel) => {\n let floatingLeg = new FloatingLegModel();\n\n unionMerge(floatingLeg, floatingLegVM);\n\n floatingLeg.notional.token = commonVM.baseCurrency;\n floatingLeg.effectiveDate = commonVM.effectiveDate;\n floatingLeg.terminationDate = commonVM.terminationDate;\n floatingLeg.dayCountBasisDay = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).day;\n floatingLeg.dayCountBasisYear = this.irsService.lookupDayCountBasis(floatingLegVM.dayCountBasis).year;\n floatingLeg.index = indexLookup[commonVM.baseCurrency];\n floatingLeg.fixingCalendar = [calendarLookup[commonVM.baseCurrency]];\n floatingLeg.paymentCalendar = [calendarLookup[commonVM.baseCurrency]];\n\n return floatingLeg;\n };\n\n toCommonModel = (commonVM: CommonViewModel) => {\n let common = new CommonModel();\n\n unionMerge(common, commonVM);\n\n common.tradeID = this.tradeId;\n common.eligibleCurrency = commonVM.baseCurrency;\n common.independentAmounts.token = commonVM.baseCurrency;\n common.threshold.token = commonVM.baseCurrency;\n common.minimumTransferAmount.token = commonVM.baseCurrency;\n common.rounding.token = commonVM.baseCurrency;\n\n return common;\n };\n\n toJson = () => {\n let commonVM = this.dealViewModel.common;\n let floatingLegVM = this.dealViewModel.floatingLeg;\n let fixedLegVM = this.dealViewModel.fixedLeg;\n\n let fixedLeg = this.toFixedLegModel(fixedLegVM, commonVM);\n let floatingLeg = this.toFloatingLegModel(floatingLegVM, commonVM);\n let common = this.toCommonModel(commonVM);\n _.assign(fixedLeg.fixedRate, fixedRateModel);\n\n let json = {\n fixedLeg: fixedLeg,\n floatingLeg: floatingLeg,\n calculation: calculationModel,\n common: common\n }\n\n return json;\n };\n};\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js index 53138a1896..228f606b8e 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js @@ -25,8 +25,8 @@ var AppComponent = (function () { this.counterparty = this.httpWrapperService.setCounterparty(value.id); }; AppComponent.prototype.renderX500Name = function (x500Name) { - var name = x500Name - x500Name.split(',').forEach(function(element) { + var name = x500Name; + x500Name.split(',').forEach(function (element) { var keyValue = element.split('='); if (keyValue[0].toUpperCase() == 'CN') { name = keyValue[1]; @@ -38,11 +38,11 @@ var AppComponent = (function () { var _this = this; this.httpWrapperService.getAbsolute("whoami").toPromise().then(function (data) { _this.whoAmI = _this.renderX500Name(data.self.text); - _this.counterParties = data.counterparties.map(function(x) { - return { + _this.counterParties = data.counterparties.map(function (x) { + return { id: x.id, - text: _this.renderX500Name(x.text) - }; + text: this.renderX500Name(x.text) + }; }); if (_this.counterParties.length == 0) { console.log("/whoami is returning no counterparties, the whole app won't run", data); diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map index 176e26eb3e..ab4cbebd9e 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js.map @@ -1 +1 @@ -{"version":3,"file":"app.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/app.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA6C,eAAe,CAAC,CAAA;AAC7D,uBAAkC,iBAAiB,CAAC,CAAA;AACpD,uBAA+C,iBAAiB,CAAC,CAAA;AACjE,2BAAkC,uBAAuB,CAAC,CAAA;AAE1D,qCAAmC,wBAAwB,CAAC,CAAA;AAgB5D;IAEE,sBAAoB,kBAAsC;QAAtC,uBAAkB,GAAlB,kBAAkB,CAAoB;QAInD,mBAAc,GAAkB,EAAE,CAAC;QAQlC,iBAAY,GAAQ,IAAI,CAAC;IAZ4B,CAAC;IAMvD,+BAAQ,GAAf,UAAgB,KAAU,IAAS,CAAC;;IAE7B,mCAAY,GAAnB,UAAoB,KAAU;QAC5B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAID,+BAAQ,GAAR;QAAA,iBAUC;QATC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YAClE,KAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAC7B,KAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC;YAC1C,EAAE,CAAC,CAAC,KAAI,CAAC,cAAc,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpC,OAAO,CAAC,GAAG,CAAC,iEAAiE,EAAE,IAAI,CAAC,CAAC;YACvF,CAAC;QACH,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,0EAA0E,EAAE,KAAK,CAAC,CAAC;QACjG,CAAC,CAAC,CAAC;IACL,CAAC;IAxCH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,UAAU;YACpB,WAAW,EAAE,oBAAoB;YACjC,SAAS,EAAE,CAAC,mBAAmB,EAAE,oDAAoD,CAAC;YACtF,UAAU,EAAE;gBACV,0BAAiB;gBACjB,gBAAO;gBACP,8BAAiB;aAClB;YACD,aAAa,EAAE,wBAAiB,CAAC,IAAI;YACrC,SAAS,EAAE,CAAC,yCAAkB,CAAC,CAAC,wDAAwD;SACzF,CAAC;;oBAAA;IA6BF,mBAAC;AAAD,CAAC,AA3BD,IA2BC;AA3BY,oBAAY,eA2BxB,CAAA","sourcesContent":["import { Component, ViewEncapsulation } from '@angular/core';\r\nimport { ROUTER_DIRECTIVES } from '@angular/router';\r\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\r\nimport { SELECT_DIRECTIVES } from 'ng2-select/ng2-select';\r\nimport * as moment from 'moment';\r\nimport { HttpWrapperService } from './http-wrapper.service';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-root',\r\n templateUrl: 'app.component.html',\r\n styleUrls: ['app.component.css', '../vendor/ng2-select/components/css/ng2-select.css'],\r\n directives: [\r\n ROUTER_DIRECTIVES,\r\n NgClass,\r\n SELECT_DIRECTIVES\r\n ],\r\n encapsulation: ViewEncapsulation.None, // allow external CSS\r\n providers: [HttpWrapperService] // don't declare in children, so that it's a \"singleton\"\r\n})\r\n\r\nexport class AppComponent {\r\n\r\n constructor(private httpWrapperService: HttpWrapperService) {}\r\n\r\n public whoAmI: string; // name\r\n public counterParty: string; // id\r\n public counterParties: Array < any > = [];\r\n\r\n public selected(value: any): void {};\r\n\r\n public refreshValue(value: any): void {\r\n this.counterparty = this.httpWrapperService.setCounterparty(value.id);\r\n }\r\n\r\n private counterparty: any = null;\r\n\r\n ngOnInit() {\r\n this.httpWrapperService.getAbsolute(\"whoami\").toPromise().then((data) => {\r\n this.whoAmI = data.self.text;\r\n this.counterParties = data.counterparties;\r\n if (this.counterParties.length == 0) {\r\n console.log(\"/whoami is returning no counterparties, the whole app won't run\", data);\r\n }\r\n }).catch((error) => {\r\n console.log(\"Error loading who am i (this is really bad, the whole app will not work)\", error);\r\n });\r\n }\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"app.component.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/app.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA6C,eAAe,CAAC,CAAA;AAC7D,uBAAkC,iBAAiB,CAAC,CAAA;AACpD,uBAA+C,iBAAiB,CAAC,CAAA;AACjE,2BAAkC,uBAAuB,CAAC,CAAA;AAE1D,qCAAmC,wBAAwB,CAAC,CAAA;AAgB5D;IAEE,sBAAoB,kBAAsC;QAAtC,uBAAkB,GAAlB,kBAAkB,CAAoB;QAInD,mBAAc,GAAkB,EAAE,CAAC;QAmBlC,iBAAY,GAAQ,IAAI,CAAC;IAvB4B,CAAC;IAMvD,+BAAQ,GAAf,UAAgB,KAAU,IAAS,CAAC;;IAE7B,mCAAY,GAAnB,UAAoB,KAAU;QAC5B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACxE,CAAC;IAEM,qCAAc,GAArB,UAAsB,QAAQ;QAC5B,IAAI,IAAI,GAAG,QAAQ,CAAC;QACpB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,OAAO;YACzC,IAAI,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAClC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC;gBACpC,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC;IACd,CAAC;IAID,+BAAQ,GAAR;QAAA,iBAeC;QAdC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YAClE,KAAI,CAAC,MAAM,GAAG,KAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClD,KAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,UAAU,CAAC;gBACrD,MAAM,CAAC;oBACH,EAAE,EAAE,CAAC,CAAC,EAAE;oBACR,IAAI,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC;iBACpC,CAAC;YACN,CAAC,CAAC,CAAC;YACH,EAAE,CAAC,CAAC,KAAI,CAAC,cAAc,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC;gBACpC,OAAO,CAAC,GAAG,CAAC,iEAAiE,EAAE,IAAI,CAAC,CAAC;YACvF,CAAC;QACH,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,0EAA0E,EAAE,KAAK,CAAC,CAAC;QACjG,CAAC,CAAC,CAAC;IACL,CAAC;IAxDH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,UAAU;YACpB,WAAW,EAAE,oBAAoB;YACjC,SAAS,EAAE,CAAC,mBAAmB,EAAE,oDAAoD,CAAC;YACtF,UAAU,EAAE;gBACV,0BAAiB;gBACjB,gBAAO;gBACP,8BAAiB;aAClB;YACD,aAAa,EAAE,wBAAiB,CAAC,IAAI;YACrC,SAAS,EAAE,CAAC,yCAAkB,CAAC,CAAC,wDAAwD;SACzF,CAAC;;oBAAA;IA6CF,mBAAC;AAAD,CAAC,AA3CD,IA2CC;AA3CY,oBAAY,eA2CxB,CAAA","sourcesContent":["import { Component, ViewEncapsulation } from '@angular/core';\nimport { ROUTER_DIRECTIVES } from '@angular/router';\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\nimport { SELECT_DIRECTIVES } from 'ng2-select/ng2-select';\nimport * as moment from 'moment';\nimport { HttpWrapperService } from './http-wrapper.service';\n\n@Component({\n moduleId: module.id,\n selector: 'app-root',\n templateUrl: 'app.component.html',\n styleUrls: ['app.component.css', '../vendor/ng2-select/components/css/ng2-select.css'],\n directives: [\n ROUTER_DIRECTIVES,\n NgClass,\n SELECT_DIRECTIVES\n ],\n encapsulation: ViewEncapsulation.None, // allow external CSS\n providers: [HttpWrapperService] // don't declare in children, so that it's a \"singleton\"\n})\n\nexport class AppComponent {\n\n constructor(private httpWrapperService: HttpWrapperService) {}\n\n public whoAmI: string; // name\n public counterParty: string; // id\n public counterParties: Array < any > = [];\n\n public selected(value: any): void {};\n\n public refreshValue(value: any): void {\n this.counterparty = this.httpWrapperService.setCounterparty(value.id);\n }\n\n public renderX500Name(x500Name) {\n var name = x500Name;\n x500Name.split(',').forEach(function (element) {\n var keyValue = element.split('=');\n if (keyValue[0].toUpperCase() == 'CN') {\n name = keyValue[1];\n }\n });\n return name;\n }\n\n private counterparty: any = null;\n\n ngOnInit() {\n this.httpWrapperService.getAbsolute(\"whoami\").toPromise().then((data) => {\n this.whoAmI = this.renderX500Name(data.self.text);\n this.counterParties = data.counterparties.map(function (x) {\n return {\n id: x.id,\n text: this.renderX500Name(x.text)\n };\n });\n if (this.counterParties.length == 0) {\n console.log(\"/whoami is returning no counterparties, the whole app won't run\", data);\n }\n }).catch((error) => {\n console.log(\"Error loading who am i (this is really bad, the whole app will not work)\", error);\n });\n }\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map index 24625e8786..cd3dcb1f5a 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"app.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/app.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,8BAA6B,iBAAiB,CAAC,CAAA;AAE/C,QAAQ,CAAC,WAAW,EAAE;IACpB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,4BAAY,CAAC,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EACxB,gBAAM,CAAC,CAAC,4BAAY,CAAC,EAAE,UAAC,GAAiB;QACvC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC,CAAC;AACR,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { AppComponent } from './app.component';\r\n\r\ndescribe('App: Vega', () => {\r\n beforeEach(() => {\r\n addProviders([AppComponent]);\r\n });\r\n\r\n it('should create the app',\r\n inject([AppComponent], (app: AppComponent) => {\r\n expect(app).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"app.component.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/app.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,8BAA6B,iBAAiB,CAAC,CAAA;AAE/C,QAAQ,CAAC,WAAW,EAAE;IACpB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,4BAAY,CAAC,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uBAAuB,EACxB,gBAAM,CAAC,CAAC,4BAAY,CAAC,EAAE,UAAC,GAAiB;QACvC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC,CAAC;AACR,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { AppComponent } from './app.component';\n\ndescribe('App: Vega', () => {\n beforeEach(() => {\n addProviders([AppComponent]);\n });\n\n it('should create the app',\n inject([AppComponent], (app: AppComponent) => {\n expect(app).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map index c33dd41d6d..268eb54ec5 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.routes.js.map @@ -1 +1 @@ -{"version":3,"file":"app.routes.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/app.routes.ts"],"names":[],"mappings":";AAAA,uBAA4C,iBAAiB,CAAC,CAAA;AAC9D,0BAAmC,aAAa,CAAC,CAAA;AACjD,2BAAoC,cAAc,CAAC,CAAA;AACnD,6BAAqC,gBAAgB,CAAC,CAAA;AACtD,2BAAmC,cAAc,CAAC,CAAA;AAElD,IAAM,MAAM,GAAiB;IAC3B,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE;IACzD,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,8BAAkB,EAAE;IACpD,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,gCAAmB,EAAE;IACtD,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,mCAAoB,EAAE;IACzD,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,+BAAkB,EAAE;CAG/D,CAAC;AAEW,0BAAkB,GAAG;IAChC,sBAAa,CAAC,MAAM,CAAC;CACtB,CAAC","sourcesContent":["import { provideRouter, RouterConfig } from '@angular/router';\r\nimport { PortfolioComponent } from './portfolio';\r\nimport { ValuationsComponent } from './valuations';\r\nimport { CreateTradeComponent } from './create-trade';\r\nimport { ViewTradeComponent } from './view-trade';\r\n\r\nconst routes: RouterConfig = [\r\n { path: '', redirectTo: '/portfolio', pathMatch: 'full' },\r\n { path: 'portfolio', component: PortfolioComponent },\r\n { path: 'valuations', component: ValuationsComponent },\r\n { path: 'create-trade', component: CreateTradeComponent },\r\n { path: 'view-trade/:tradeId', component: ViewTradeComponent }\r\n\r\n // { path: '**', component: PageNotFoundComponent }\r\n];\r\n\r\nexport const appRouterProviders = [\r\n provideRouter(routes)\r\n];\r\n"]} \ No newline at end of file +{"version":3,"file":"app.routes.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/app.routes.ts"],"names":[],"mappings":";AAAA,uBAA4C,iBAAiB,CAAC,CAAA;AAC9D,0BAAmC,aAAa,CAAC,CAAA;AACjD,2BAAoC,cAAc,CAAC,CAAA;AACnD,6BAAqC,gBAAgB,CAAC,CAAA;AACtD,2BAAmC,cAAc,CAAC,CAAA;AAElD,IAAM,MAAM,GAAiB;IAC3B,EAAE,IAAI,EAAE,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE;IACzD,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,8BAAkB,EAAE;IACpD,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,gCAAmB,EAAE;IACtD,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,mCAAoB,EAAE;IACzD,EAAE,IAAI,EAAE,qBAAqB,EAAE,SAAS,EAAE,+BAAkB,EAAE;CAG/D,CAAC;AAEW,0BAAkB,GAAG;IAChC,sBAAa,CAAC,MAAM,CAAC;CACtB,CAAC","sourcesContent":["import { provideRouter, RouterConfig } from '@angular/router';\nimport { PortfolioComponent } from './portfolio';\nimport { ValuationsComponent } from './valuations';\nimport { CreateTradeComponent } from './create-trade';\nimport { ViewTradeComponent } from './view-trade';\n\nconst routes: RouterConfig = [\n { path: '', redirectTo: '/portfolio', pathMatch: 'full' },\n { path: 'portfolio', component: PortfolioComponent },\n { path: 'valuations', component: ValuationsComponent },\n { path: 'create-trade', component: CreateTradeComponent },\n { path: 'view-trade/:tradeId', component: ViewTradeComponent }\n\n // { path: '**', component: PageNotFoundComponent }\n];\n\nexport const appRouterProviders = [\n provideRouter(routes)\n];\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map index 91a048ca80..54e5996fbe 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.js.map @@ -1 +1 @@ -{"version":3,"file":"create-trade.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/create-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,4BAA2B,gBAAgB,CAAC,CAAA;AAC5C,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAyB,iBAAiB,CAAC,CAAA;AAC3C,uBAAuB,iBAAiB,CAAC,CAAA;AACzC,qCAAmC,yBAAyB,CAAC,CAAA;AAE7D;IAAA;QACE,OAAE,GAAW,MAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;QAI1D,eAAU,GAAW,uBAAuB,CAAC;QAG7C,YAAO,GAAW,KAAK,CAAC;QACxB,aAAQ,GAAW,SAAS,CAAC;QAC7B,cAAS,GAAW,OAAO,CAAC;IAC9B,CAAC;IAAD,iBAAC;AAAD,CAAC,AAXD,IAWC;AASD;IAKE,8BACU,UAAsB,EACtB,WAAwB,EACxB,QAAkB,EAClB,MAAc,EACd,kBAAsC;QAVlD,iBAiCC;QA3BW,eAAU,GAAV,UAAU,CAAY;QACtB,gBAAW,GAAX,WAAW,CAAa;QACxB,aAAQ,GAAR,QAAQ,CAAU;QAClB,WAAM,GAAN,MAAM,CAAQ;QACd,uBAAkB,GAAlB,kBAAkB,CAAoB;QAPhD,cAAS,GAAW,EAAE,CAAC;QAoBvB,eAAU,GAAG;YACX,IAAI,IAAI,GAAG,KAAI,CAAC;YAChB,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,QAAQ,EAAE,KAAI,CAAC,IAAI,CAAC;iBAC7D,SAAS,EAAE,CAAC,IAAI,CAAC;gBAChB,KAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAe,KAAI,CAAC,IAAI,CAAC,EAAI,CAAC,CAAC;YAC3D,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;gBACb,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACzB,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;QAnBA,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7E,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,yBAAyB,CAAC;QACjD,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,aAAa,CAAC;IACxC,CAAC;IAED,uCAAQ,GAAR,cAAY,CAAC;IA5Bf;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,kBAAkB;YAC5B,WAAW,EAAE,6BAA6B;YAC1C,SAAS,EAAE,CAAC,sBAAsB,EAAE,4BAA4B,CAAC;YACjE,SAAS,EAAE,CAAC,wBAAU,EAAE,0BAAW,EAAE,iBAAQ,CAAC;SAC/C,CAAC;;4BAAA;IAkCF,2BAAC;AAAD,CAAC,AAjCD,IAiCC;AAjCY,4BAAoB,uBAiChC,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\r\nimport { IRSService } from '../irs.service';\r\nimport { NodeService } from '../node.service';\r\nimport { Location } from '@angular/common';\r\nimport { Router } from '@angular/router';\r\nimport { HttpWrapperService } from '../http-wrapper.service';\r\n\r\nclass DealParams {\r\n id: string = `${100 + Math.floor((Math.random() * 900))}`;\r\n description: string;\r\n counterparty: string;\r\n tradeDate: string;\r\n convention: string = \"USD_FIXED_6M_LIBOR_3M\";\r\n startDate: string;\r\n endDate: string;\r\n buySell: string = \"BUY\";\r\n notional: string = \"1000000\";\r\n fixedRate: string = \"0.015\";\r\n}\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-create-trade',\r\n templateUrl: 'create-trade.component.html',\r\n styleUrls: ['../app.component.css', 'create-trade.component.css'],\r\n providers: [IRSService, NodeService, Location]\r\n})\r\nexport class CreateTradeComponent implements OnInit {\r\n dayCountBasisLookup: string[];\r\n deal: DealParams;\r\n formError: string = \"\";\r\n\r\n constructor(\r\n private irsService: IRSService,\r\n private nodeService: NodeService,\r\n private location: Location,\r\n private router: Router,\r\n private httpWrapperService: HttpWrapperService\r\n ) {\r\n this.dayCountBasisLookup = Object.keys(this.irsService.lookupTable);\r\n this.deal = new DealParams();\r\n this.deal.tradeDate = this.nodeService.formatDateForNode(new Date());\r\n this.deal.startDate = this.nodeService.formatDateForNode(new Date());\r\n this.deal.endDate = this.nodeService.formatDateForNode(new Date(2020, 1, 1));\r\n this.deal.convention = \"EUR_FIXED_1Y_EURIBOR_3M\";\r\n this.deal.description = \"description\";\r\n }\r\n\r\n ngOnInit() {}\r\n\r\n createDeal = () => {\r\n var that = this;\r\n this.httpWrapperService.putWithCounterparty(\"trades\", this.deal)\r\n .toPromise().then(() => {\r\n this.router.navigateByUrl(`/view-trade/${this.deal.id}`);\r\n }).catch((error) => {\r\n that.formError = error;\r\n });\r\n };\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"create-trade.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/create-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,4BAA2B,gBAAgB,CAAC,CAAA;AAC5C,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAyB,iBAAiB,CAAC,CAAA;AAC3C,uBAAuB,iBAAiB,CAAC,CAAA;AACzC,qCAAmC,yBAAyB,CAAC,CAAA;AAE7D;IAAA;QACE,OAAE,GAAW,MAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;QAI1D,eAAU,GAAW,uBAAuB,CAAC;QAG7C,YAAO,GAAW,KAAK,CAAC;QACxB,aAAQ,GAAW,SAAS,CAAC;QAC7B,cAAS,GAAW,OAAO,CAAC;IAC9B,CAAC;IAAD,iBAAC;AAAD,CAAC,AAXD,IAWC;AASD;IAKE,8BACU,UAAsB,EACtB,WAAwB,EACxB,QAAkB,EAClB,MAAc,EACd,kBAAsC;QAVlD,iBAiCC;QA3BW,eAAU,GAAV,UAAU,CAAY;QACtB,gBAAW,GAAX,WAAW,CAAa;QACxB,aAAQ,GAAR,QAAQ,CAAU;QAClB,WAAM,GAAN,MAAM,CAAQ;QACd,uBAAkB,GAAlB,kBAAkB,CAAoB;QAPhD,cAAS,GAAW,EAAE,CAAC;QAoBvB,eAAU,GAAG;YACX,IAAI,IAAI,GAAG,KAAI,CAAC;YAChB,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,QAAQ,EAAE,KAAI,CAAC,IAAI,CAAC;iBAC7D,SAAS,EAAE,CAAC,IAAI,CAAC;gBAChB,KAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAe,KAAI,CAAC,IAAI,CAAC,EAAI,CAAC,CAAC;YAC3D,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;gBACb,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;YACzB,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;QAnBA,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7E,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,yBAAyB,CAAC;QACjD,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,aAAa,CAAC;IACxC,CAAC;IAED,uCAAQ,GAAR,cAAY,CAAC;IA5Bf;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,kBAAkB;YAC5B,WAAW,EAAE,6BAA6B;YAC1C,SAAS,EAAE,CAAC,sBAAsB,EAAE,4BAA4B,CAAC;YACjE,SAAS,EAAE,CAAC,wBAAU,EAAE,0BAAW,EAAE,iBAAQ,CAAC;SAC/C,CAAC;;4BAAA;IAkCF,2BAAC;AAAD,CAAC,AAjCD,IAiCC;AAjCY,4BAAoB,uBAiChC,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\nimport { IRSService } from '../irs.service';\nimport { NodeService } from '../node.service';\nimport { Location } from '@angular/common';\nimport { Router } from '@angular/router';\nimport { HttpWrapperService } from '../http-wrapper.service';\n\nclass DealParams {\n id: string = `${100 + Math.floor((Math.random() * 900))}`;\n description: string;\n counterparty: string;\n tradeDate: string;\n convention: string = \"USD_FIXED_6M_LIBOR_3M\";\n startDate: string;\n endDate: string;\n buySell: string = \"BUY\";\n notional: string = \"1000000\";\n fixedRate: string = \"0.015\";\n}\n\n@Component({\n moduleId: module.id,\n selector: 'app-create-trade',\n templateUrl: 'create-trade.component.html',\n styleUrls: ['../app.component.css', 'create-trade.component.css'],\n providers: [IRSService, NodeService, Location]\n})\nexport class CreateTradeComponent implements OnInit {\n dayCountBasisLookup: string[];\n deal: DealParams;\n formError: string = \"\";\n\n constructor(\n private irsService: IRSService,\n private nodeService: NodeService,\n private location: Location,\n private router: Router,\n private httpWrapperService: HttpWrapperService\n ) {\n this.dayCountBasisLookup = Object.keys(this.irsService.lookupTable);\n this.deal = new DealParams();\n this.deal.tradeDate = this.nodeService.formatDateForNode(new Date());\n this.deal.startDate = this.nodeService.formatDateForNode(new Date());\n this.deal.endDate = this.nodeService.formatDateForNode(new Date(2020, 1, 1));\n this.deal.convention = \"EUR_FIXED_1Y_EURIBOR_3M\";\n this.deal.description = \"description\";\n }\n\n ngOnInit() {}\n\n createDeal = () => {\n var that = this;\n this.httpWrapperService.putWithCounterparty(\"trades\", this.deal)\n .toPromise().then(() => {\n this.router.navigateByUrl(`/view-trade/${this.deal.id}`);\n }).catch((error) => {\n that.formError = error;\n });\n };\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map index 0b53d9d455..32b7d22eed 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/create-trade.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"create-trade.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/create-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,wBAAwB,EAAE;IACjC,EAAE,CAAC,2BAA2B,EAAE;QAC9B,6CAA6C;QAC7C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { CreateTradeComponent } from './create-trade.component';\r\n\r\ndescribe('Component: CreateTrade', () => {\r\n it('should create an instance', () => {\r\n //let component = new CreateTradeComponent();\r\n //expect(component).toBeTruthy();\r\n });\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"create-trade.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/create-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,wBAAwB,EAAE;IACjC,EAAE,CAAC,2BAA2B,EAAE;QAC9B,6CAA6C;QAC7C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { CreateTradeComponent } from './create-trade.component';\n\ndescribe('Component: CreateTrade', () => {\n it('should create an instance', () => {\n //let component = new CreateTradeComponent();\n //expect(component).toBeTruthy();\n });\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map index a01bb2b88c..ecb07b7343 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,0BAA0B,CAAC,EAAA","sourcesContent":["export * from './create-trade.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,0BAA0B,CAAC,EAAA","sourcesContent":["export * from './create-trade.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map index 9a1fb10668..6c8ed6fe10 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/create-trade/shared/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/create-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/create-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map index 3fda85efe4..9c5819dfac 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/environment.js.map @@ -1 +1 @@ -{"version":3,"file":"environment.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/environment.ts"],"names":[],"mappings":";AAAa,mBAAW,GAAG;IACzB,UAAU,EAAE,KAAK;IACjB,OAAO,EAAE,yBAAyB;CACnC,CAAC","sourcesContent":["export const environment = {\r\n production: false,\r\n APIPath: \"/api/simmvaluationdemo/\"\r\n};\r\n"]} \ No newline at end of file +{"version":3,"file":"environment.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/environment.ts"],"names":[],"mappings":";AAAa,mBAAW,GAAG;IACzB,UAAU,EAAE,KAAK;IACjB,OAAO,EAAE,yBAAyB;CACnC,CAAC","sourcesContent":["export const environment = {\n production: false,\n APIPath: \"/api/simmvaluationdemo/\"\n};\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map index 495c039532..12f56275ab 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.js.map @@ -1 +1 @@ -{"version":3,"file":"http-wrapper.service.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/http-wrapper.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAyC,eAAe,CAAC,CAAA;AACzD,uBAAsC,iBAAiB,CAAC,CAAA;AACxD,qBAAqB,eAAe,CAAC,CAAA;AACrC,4BAA4B,eAAe,CAAC,CAAA;AAC5C,mBAA2B,SAAS,CAAC,CAAA;AAGrC;IAIE,4BAAoB,IAAU,EAAU,MAAc;QAJxD,iBAoIC;QAhIqB,SAAI,GAAJ,IAAI,CAAM;QAAU,WAAM,GAAN,MAAM,CAAQ;QAmB/C,oBAAe,GAAyB,IAAI,mBAAY,EAAE,CAAC;QAuD1D,SAAI,GAAW,CAAC,CAAC;QAzEvB,gDAAgD;QAChD,+DAA+D;QAC/D,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,UAAC,KAAK;YAC5B,EAAE,CAAC,CAAC,KAAK,YAAY,sBAAa,CAAC,CAAC,CAAC;gBACnC,KAAI,CAAC,mBAAmB,EAAE,CAAC;YAC7B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,eAAe;IAEP,gDAAmB,GAA3B;QACE,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;YACtB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC;gBACxB,KAAK,EAAE,IAAI,CAAC,YAAY;aACzB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAGD,oBAAoB;IAGpB,uBAAuB;IAEhB,4CAAe,GAAtB,UAAuB,EAAE;QACvB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW;IACxB,CAAC;IACM,4CAAe,GAAtB;QACE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,2BAA2B;IAG3B,eAAe;IAEP,oCAAO,GAAf,UAAgB,QAAQ;QACtB,MAAM,CAAC,yBAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED,mBAAmB;IAGnB,eAAe;IAER,gDAAmB,GAA1B,UAA2B,QAAQ;QACjC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IAChG,CAAC;IAEM,iDAAoB,GAA3B,UAA4B,QAAQ,EAAE,IAAI;QACxC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,GAAG,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IACvG,CAAC;IAEM,gDAAmB,GAA1B,UAA2B,QAAQ,EAAE,IAAI;QACvC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,GAAG,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IACtG,CAAC;IAEM,wCAAW,GAAlB,UAAmB,QAAQ;QACzB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IACtE,CAAC;IAYO,8CAAiB,GAAzB,UAA0B,IAAI;QAC5B,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YACpB,MAAM,CAAC,CAAC,oDAAoD;QAC9D,CAAC;QAED,IAAI,WAAW,GAAQ,EAAE,CAAC;QAE1B,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;YACvC,WAAW,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACjD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;YACzC,WAAW,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QAClD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;YAC/C,WAAW,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACrD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;YAC/C,WAAW,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACrD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;YAC7C,WAAW,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACpD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YACnB,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;QAClC,CAAC;QACD,MAAM,CAAC,WAAW,CAAC;IACrB,CAAC;IAEM,8CAAiB,GAAxB;QAAA,iBAMC;QALC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QAEd,8BAA8B;QAC9B,IAAI,KAAK,GAAG,eAAU,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,UAAA,CAAC,IAAM,KAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IAEM,2CAAc,GAArB,UAAsB,IAAI;QACxB,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAA;IACrC,CAAC;IAhIH;QAAC,iBAAU,EAAE;;0BAAA;IAqIb,yBAAC;AAAD,CAAC,AApID,IAoIC;AApIY,0BAAkB,qBAoI9B,CAAA","sourcesContent":["import { Injectable, EventEmitter } from '@angular/core';\r\nimport { Router, NavigationEnd } from '@angular/router';\r\nimport { Http } from '@angular/http';\r\nimport { environment } from './environment';\r\nimport { Observable } from 'rxjs/Rx';\r\n\r\n@Injectable()\r\nexport class HttpWrapperService {\r\n\r\n private counterparty: string;\r\n\r\n constructor(private http: Http, private router: Router) {\r\n // because components listen on newCounterparty,\r\n // they need to know there is a new value when view is switched\r\n router.events.subscribe((event) => {\r\n if (event instanceof NavigationEnd) { //NavigationEnd?\r\n this.emitNewCounterparty();\r\n }\r\n });\r\n }\r\n\r\n //new CP events\r\n\r\n private emitNewCounterparty() {\r\n if (this.counterparty) {\r\n this.newCounterparty.emit({\r\n value: this.counterparty\r\n });\r\n }\r\n }\r\n public newCounterparty: EventEmitter < any > = new EventEmitter();\r\n\r\n // end new CP events\r\n\r\n\r\n // CP getter and setter\r\n\r\n public setCounterparty(cp) {\r\n this.counterparty = cp;\r\n this.emitNewCounterparty();\r\n return cp; //chainable\r\n }\r\n public getCounterparty() {\r\n return this.counterparty;\r\n }\r\n\r\n // end CP getter and setter\r\n\r\n\r\n // HTTP helpers\r\n\r\n private getPath(resource) {\r\n return environment.APIPath + resource;\r\n }\r\n\r\n // end HTTP helpers\r\n\r\n\r\n // HTTP methods\r\n\r\n public getWithCounterparty(resource): any {\r\n return this.http.get(this.getPath(this.counterparty + \"/\" + resource)).map(res => res.json());\r\n }\r\n\r\n public postWithCounterparty(resource, data): any {\r\n return this.http.post(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\r\n }\r\n\r\n public putWithCounterparty(resource, data): any {\r\n return this.http.put(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\r\n }\r\n\r\n public getAbsolute(resource): any {\r\n return this.http.get(this.getPath(resource)).map(res => res.json());\r\n }\r\n\r\n // end HTTP methods\r\n\r\n\r\n\r\n // *****************************************\r\n // Demo magic - delayed data for valuations\r\n // *****************************************\r\n\r\n private subscription;\r\n private step: number = 0;\r\n private updateDelayedData(data) {\r\n if (!data.portfolio) {\r\n return; // data hasn't fully returned yet, don't do anything\r\n }\r\n\r\n var delayedData: any = {};\r\n\r\n if(this.step > 0) {\r\n delayedData.portfolio = data.portfolio;\r\n delayedData.portfolio.agreed = (this.step > 1);\r\n }\r\n\r\n if(this.step > 2) {\r\n delayedData.marketData = data.marketData;\r\n delayedData.marketData.agreed = (this.step > 3);\r\n }\r\n\r\n if(this.step > 4) {\r\n delayedData.sensitivities = data.sensitivities;\r\n delayedData.sensitivities.agreed = (this.step > 5);\r\n }\r\n\r\n if(this.step > 6) {\r\n delayedData.initialMargin = data.initialMargin;\r\n delayedData.initialMargin.agreed = (this.step > 7);\r\n }\r\n\r\n if(this.step > 8) {\r\n delayedData.confirmation = data.confirmation;\r\n delayedData.confirmation.agreed = (this.step > 9);\r\n }\r\n\r\n if(this.step == 10) {\r\n this.subscription.unsubscribe();\r\n }\r\n return delayedData;\r\n }\r\n\r\n public startDelayedTimer() {\r\n this.step = 0;\r\n\r\n // every x second, update data\r\n let timer = Observable.timer(1000, 2000);\r\n this.subscription = timer.subscribe(t => { this.step++; });\r\n }\r\n\r\n public getDelayedData(data): any {\r\n return this.updateDelayedData(data)\r\n }\r\n\r\n // *****************************************\r\n // end demo magic\r\n // *****************************************\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"http-wrapper.service.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/http-wrapper.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAyC,eAAe,CAAC,CAAA;AACzD,uBAAsC,iBAAiB,CAAC,CAAA;AACxD,qBAAqB,eAAe,CAAC,CAAA;AACrC,4BAA4B,eAAe,CAAC,CAAA;AAC5C,mBAA2B,SAAS,CAAC,CAAA;AAGrC;IAIE,4BAAoB,IAAU,EAAU,MAAc;QAJxD,iBAoIC;QAhIqB,SAAI,GAAJ,IAAI,CAAM;QAAU,WAAM,GAAN,MAAM,CAAQ;QAmB/C,oBAAe,GAAyB,IAAI,mBAAY,EAAE,CAAC;QAuD1D,SAAI,GAAW,CAAC,CAAC;QAzEvB,gDAAgD;QAChD,+DAA+D;QAC/D,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,UAAC,KAAK;YAC5B,EAAE,CAAC,CAAC,KAAK,YAAY,sBAAa,CAAC,CAAC,CAAC;gBACnC,KAAI,CAAC,mBAAmB,EAAE,CAAC;YAC7B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,eAAe;IAEP,gDAAmB,GAA3B;QACE,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;YACtB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC;gBACxB,KAAK,EAAE,IAAI,CAAC,YAAY;aACzB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAGD,oBAAoB;IAGpB,uBAAuB;IAEhB,4CAAe,GAAtB,UAAuB,EAAE;QACvB,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW;IACxB,CAAC;IACM,4CAAe,GAAtB;QACE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,2BAA2B;IAG3B,eAAe;IAEP,oCAAO,GAAf,UAAgB,QAAQ;QACtB,MAAM,CAAC,yBAAW,CAAC,OAAO,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED,mBAAmB;IAGnB,eAAe;IAER,gDAAmB,GAA1B,UAA2B,QAAQ;QACjC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IAChG,CAAC;IAEM,iDAAoB,GAA3B,UAA4B,QAAQ,EAAE,IAAI;QACxC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,GAAG,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IACvG,CAAC;IAEM,gDAAmB,GAA1B,UAA2B,QAAQ,EAAE,IAAI;QACvC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,GAAG,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IACtG,CAAC;IAEM,wCAAW,GAAlB,UAAmB,QAAQ;QACzB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAA,GAAG,CAAC,IAAI,EAAE,EAAV,CAAU,CAAC,CAAC;IACtE,CAAC;IAYO,8CAAiB,GAAzB,UAA0B,IAAI;QAC5B,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YACpB,MAAM,CAAC,CAAC,oDAAoD;QAC9D,CAAC;QAED,IAAI,WAAW,GAAQ,EAAE,CAAC;QAE1B,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;YACvC,WAAW,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACjD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;YACzC,WAAW,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QAClD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;YAC/C,WAAW,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACrD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;YAC/C,WAAW,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACrD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACjB,WAAW,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;YAC7C,WAAW,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;QACpD,CAAC;QAED,EAAE,CAAA,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YACnB,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;QAClC,CAAC;QACD,MAAM,CAAC,WAAW,CAAC;IACrB,CAAC;IAEM,8CAAiB,GAAxB;QAAA,iBAMC;QALC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QAEd,8BAA8B;QAC9B,IAAI,KAAK,GAAG,eAAU,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,SAAS,CAAC,UAAA,CAAC,IAAM,KAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IAEM,2CAAc,GAArB,UAAsB,IAAI;QACxB,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAA;IACrC,CAAC;IAhIH;QAAC,iBAAU,EAAE;;0BAAA;IAqIb,yBAAC;AAAD,CAAC,AApID,IAoIC;AApIY,0BAAkB,qBAoI9B,CAAA","sourcesContent":["import { Injectable, EventEmitter } from '@angular/core';\nimport { Router, NavigationEnd } from '@angular/router';\nimport { Http } from '@angular/http';\nimport { environment } from './environment';\nimport { Observable } from 'rxjs/Rx';\n\n@Injectable()\nexport class HttpWrapperService {\n\n private counterparty: string;\n\n constructor(private http: Http, private router: Router) {\n // because components listen on newCounterparty,\n // they need to know there is a new value when view is switched\n router.events.subscribe((event) => {\n if (event instanceof NavigationEnd) { //NavigationEnd?\n this.emitNewCounterparty();\n }\n });\n }\n\n //new CP events\n\n private emitNewCounterparty() {\n if (this.counterparty) {\n this.newCounterparty.emit({\n value: this.counterparty\n });\n }\n }\n public newCounterparty: EventEmitter < any > = new EventEmitter();\n\n // end new CP events\n\n\n // CP getter and setter\n\n public setCounterparty(cp) {\n this.counterparty = cp;\n this.emitNewCounterparty();\n return cp; //chainable\n }\n public getCounterparty() {\n return this.counterparty;\n }\n\n // end CP getter and setter\n\n\n // HTTP helpers\n\n private getPath(resource) {\n return environment.APIPath + resource;\n }\n\n // end HTTP helpers\n\n\n // HTTP methods\n\n public getWithCounterparty(resource): any {\n return this.http.get(this.getPath(this.counterparty + \"/\" + resource)).map(res => res.json());\n }\n\n public postWithCounterparty(resource, data): any {\n return this.http.post(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\n }\n\n public putWithCounterparty(resource, data): any {\n return this.http.put(this.getPath(this.counterparty + \"/\" + resource), data).map(res => res.json());\n }\n\n public getAbsolute(resource): any {\n return this.http.get(this.getPath(resource)).map(res => res.json());\n }\n\n // end HTTP methods\n\n\n\n // *****************************************\n // Demo magic - delayed data for valuations\n // *****************************************\n\n private subscription;\n private step: number = 0;\n private updateDelayedData(data) {\n if (!data.portfolio) {\n return; // data hasn't fully returned yet, don't do anything\n }\n\n var delayedData: any = {};\n\n if(this.step > 0) {\n delayedData.portfolio = data.portfolio;\n delayedData.portfolio.agreed = (this.step > 1);\n }\n\n if(this.step > 2) {\n delayedData.marketData = data.marketData;\n delayedData.marketData.agreed = (this.step > 3);\n }\n\n if(this.step > 4) {\n delayedData.sensitivities = data.sensitivities;\n delayedData.sensitivities.agreed = (this.step > 5);\n }\n\n if(this.step > 6) {\n delayedData.initialMargin = data.initialMargin;\n delayedData.initialMargin.agreed = (this.step > 7);\n }\n\n if(this.step > 8) {\n delayedData.confirmation = data.confirmation;\n delayedData.confirmation.agreed = (this.step > 9);\n }\n\n if(this.step == 10) {\n this.subscription.unsubscribe();\n }\n return delayedData;\n }\n\n public startDelayedTimer() {\n this.step = 0;\n\n // every x second, update data\n let timer = Observable.timer(1000, 2000);\n this.subscription = timer.subscribe(t => { this.step++; });\n }\n\n public getDelayedData(data): any {\n return this.updateDelayedData(data)\n }\n\n // *****************************************\n // end demo magic\n // *****************************************\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map index 14e1a28e30..532f3fffb7 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/http-wrapper.service.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"http-wrapper.service.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/http-wrapper.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,qCAAmC,wBAAwB,CAAC,CAAA;AAE5D,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,yCAAkB,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,yCAAkB,CAAC,EACzB,UAAC,OAA2B;QAC1B,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { HttpWrapperService } from './http-wrapper.service';\r\n\r\ndescribe('Service: HttpWrapper', () => {\r\n beforeEach(() => {\r\n addProviders([HttpWrapperService]);\r\n });\r\n\r\n it('should ...',\r\n inject([HttpWrapperService],\r\n (service: HttpWrapperService) => {\r\n expect(service).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"http-wrapper.service.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/http-wrapper.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,qCAAmC,wBAAwB,CAAC,CAAA;AAE5D,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,yCAAkB,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,yCAAkB,CAAC,EACzB,UAAC,OAA2B;QAC1B,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { HttpWrapperService } from './http-wrapper.service';\n\ndescribe('Service: HttpWrapper', () => {\n beforeEach(() => {\n addProviders([HttpWrapperService]);\n });\n\n it('should ...',\n inject([HttpWrapperService],\n (service: HttpWrapperService) => {\n expect(service).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map index e052eb7c5a..16ec595c18 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,eAAe,CAAC,EAAA;AAC9B,iBAAc,iBAAiB,CAAC,EAAA","sourcesContent":["export * from './environment';\r\nexport * from './app.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,eAAe,CAAC,EAAA;AAC9B,iBAAc,iBAAiB,CAAC,EAAA","sourcesContent":["export * from './environment';\nexport * from './app.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map index 813b4bbf55..889e9b5b79 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.js.map @@ -1 +1 @@ -{"version":3,"file":"irs.service.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/irs.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAE3C;IACE,uBAAmB,GAAW,EAAS,IAAY;QAAhC,QAAG,GAAH,GAAG,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IACzD,oBAAC;AAAD,CAAC,AAFD,IAEC;AAFY,qBAAa,gBAEzB,CAAA;AAGD;IAWE;QAXF,iBAiBC;QAhBC,gBAAW,GAAG;YACZ,QAAQ,EAAE,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC;YAC1C,SAAS,EAAE,IAAI,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC;YAC5C,SAAS,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC;YAC/C,eAAe,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACtD,WAAW,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YAClD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACrD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;SACtD,CAAA;QAID,wBAAmB,GAAa,UAAC,SAAiB;YAChD,MAAM,CAAC,KAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC,CAAA;IAJc,CAAC;IAZlB;QAAC,iBAAU,EAAE;;kBAAA;IAkBb,iBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,kBAAU,aAiBtB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\r\n\r\nexport class DayCountBasis {\r\n constructor(public day: string, public year: string) {}\r\n}\r\n\r\n@Injectable()\r\nexport class IRSService {\r\n lookupTable = {\r\n \"30/360\": new DayCountBasis(\"D30\", \"Y360\"),\r\n \"30E/360\": new DayCountBasis(\"D30E\", \"Y360\"),\r\n \"ACT/360\": new DayCountBasis(\"DActual\", \"Y360\"),\r\n \"ACT/365 Fixed\": new DayCountBasis(\"DActual\", \"Y365F\"),\r\n \"ACT/365 L\": new DayCountBasis(\"DActual\", \"Y365L\"),\r\n \"ACT/ACT ISDA\": new DayCountBasis(\"DActual\", \"YISDA\"),\r\n \"ACT/ACT ICMA\": new DayCountBasis(\"DActual\", \"YICMA\")\r\n }\r\n\r\n constructor() {}\r\n\r\n lookupDayCountBasis: Function = (shorthand: string) => {\r\n return this.lookupTable[shorthand];\r\n }\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"irs.service.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/irs.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAE3C;IACE,uBAAmB,GAAW,EAAS,IAAY;QAAhC,QAAG,GAAH,GAAG,CAAQ;QAAS,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IACzD,oBAAC;AAAD,CAAC,AAFD,IAEC;AAFY,qBAAa,gBAEzB,CAAA;AAGD;IAWE;QAXF,iBAiBC;QAhBC,gBAAW,GAAG;YACZ,QAAQ,EAAE,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC;YAC1C,SAAS,EAAE,IAAI,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC;YAC5C,SAAS,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC;YAC/C,eAAe,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACtD,WAAW,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YAClD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;YACrD,cAAc,EAAE,IAAI,aAAa,CAAC,SAAS,EAAE,OAAO,CAAC;SACtD,CAAA;QAID,wBAAmB,GAAa,UAAC,SAAiB;YAChD,MAAM,CAAC,KAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC,CAAA;IAJc,CAAC;IAZlB;QAAC,iBAAU,EAAE;;kBAAA;IAkBb,iBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,kBAAU,aAiBtB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\n\nexport class DayCountBasis {\n constructor(public day: string, public year: string) {}\n}\n\n@Injectable()\nexport class IRSService {\n lookupTable = {\n \"30/360\": new DayCountBasis(\"D30\", \"Y360\"),\n \"30E/360\": new DayCountBasis(\"D30E\", \"Y360\"),\n \"ACT/360\": new DayCountBasis(\"DActual\", \"Y360\"),\n \"ACT/365 Fixed\": new DayCountBasis(\"DActual\", \"Y365F\"),\n \"ACT/365 L\": new DayCountBasis(\"DActual\", \"Y365L\"),\n \"ACT/ACT ISDA\": new DayCountBasis(\"DActual\", \"YISDA\"),\n \"ACT/ACT ICMA\": new DayCountBasis(\"DActual\", \"YICMA\")\n }\n\n constructor() {}\n\n lookupDayCountBasis: Function = (shorthand: string) => {\n return this.lookupTable[shorthand];\n }\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map index 6d6f88cf21..970745e9c4 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/irs.service.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"irs.service.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/irs.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,4BAA2B,eAAe,CAAC,CAAA;AAE3C,QAAQ,CAAC,cAAc,EAAE;IACvB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,wBAAU,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,wBAAU,CAAC,EACjB,UAAC,OAAmB;QAClB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { IRSService } from './irs.service';\r\n\r\ndescribe('Service: IRS', () => {\r\n beforeEach(() => {\r\n addProviders([IRSService]);\r\n });\r\n\r\n it('should ...',\r\n inject([IRSService],\r\n (service: IRSService) => {\r\n expect(service).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"irs.service.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/irs.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,4BAA2B,eAAe,CAAC,CAAA;AAE3C,QAAQ,CAAC,cAAc,EAAE;IACvB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,wBAAU,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,wBAAU,CAAC,EACjB,UAAC,OAAmB;QAClB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { IRSService } from './irs.service';\n\ndescribe('Service: IRS', () => {\n beforeEach(() => {\n addProviders([IRSService]);\n });\n\n it('should ...',\n inject([IRSService],\n (service: IRSService) => {\n expect(service).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js index 581b2feed4..88b73e6f81 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js @@ -23,7 +23,6 @@ var CommonModel = (function () { this.exposure = null; this.localBusinessDay = null; this.dailyInterestAmount = null; - this.hashLegalDocs = null; this.tradeID = null; } return CommonModel; diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map index 5e44b6f2f4..d3d3c98ead 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/CommonModel.js.map @@ -1 +1 @@ -{"version":3,"file":"CommonModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/model/CommonModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAW,IAAI,CAAC;QAC5B,0BAAqB,GAAW,IAAI,CAAC;QACrC,uBAAkB,GAAG;YACnB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,cAAS,GAAG;YACV,KAAK,EAAE,EAAE;SACV,CAAC;QACF,0BAAqB,GAAG;YACtB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,kBAAa,GAAW,IAAI,CAAC;QAC7B,qBAAgB,GAAW,IAAI,CAAC;QAChC,mBAAc,GAAW,IAAI,CAAC;QAC9B,iBAAY,GAAW,IAAI,CAAC;QAC5B,wBAAmB,GAAW,IAAI,CAAC;QACnC,aAAQ,GAAW,IAAI,CAAC;QACxB,qBAAgB,GAAW,IAAI,CAAC;QAChC,wBAAmB,GAAW,IAAI,CAAC;QACnC,kBAAa,GAAW,IAAI,CAAC;QAC7B,YAAO,GAAW,IAAI,CAAC;IAEzB,CAAC;IAAD,kBAAC;AAAD,CAAC,AA1BD,IA0BC;AA1BY,mBAAW,cA0BvB,CAAA","sourcesContent":["export class CommonModel {\r\n baseCurrency: string = null;\r\n eligibleCreditSupport: string = null;\r\n independentAmounts = {\r\n token: \"\"\r\n };\r\n threshold = {\r\n token: \"\"\r\n };\r\n minimumTransferAmount = {\r\n token: \"\"\r\n };\r\n rounding = {\r\n token: \"\"\r\n };\r\n valuationDate: string = null;\r\n notificationTime: string = null;\r\n resolutionTime: string = null;\r\n interestRate: Object = null;\r\n addressForTransfers: string = null;\r\n exposure: Object = null;\r\n localBusinessDay: Object = null;\r\n dailyInterestAmount: string = null;\r\n hashLegalDocs: string = null;\r\n tradeID: string = null;\r\n eligibleCurrency: string;\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"CommonModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/model/CommonModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAW,IAAI,CAAC;QAC5B,0BAAqB,GAAW,IAAI,CAAC;QACrC,uBAAkB,GAAG;YACnB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,cAAS,GAAG;YACV,KAAK,EAAE,EAAE;SACV,CAAC;QACF,0BAAqB,GAAG;YACtB,KAAK,EAAE,EAAE;SACV,CAAC;QACF,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,kBAAa,GAAW,IAAI,CAAC;QAC7B,qBAAgB,GAAW,IAAI,CAAC;QAChC,mBAAc,GAAW,IAAI,CAAC;QAC9B,iBAAY,GAAW,IAAI,CAAC;QAC5B,wBAAmB,GAAW,IAAI,CAAC;QACnC,aAAQ,GAAW,IAAI,CAAC;QACxB,qBAAgB,GAAW,IAAI,CAAC;QAChC,wBAAmB,GAAW,IAAI,CAAC;QACnC,YAAO,GAAW,IAAI,CAAC;IAEzB,CAAC;IAAD,kBAAC;AAAD,CAAC,AAzBD,IAyBC;AAzBY,mBAAW,cAyBvB,CAAA","sourcesContent":["export class CommonModel {\n baseCurrency: string = null;\n eligibleCreditSupport: string = null;\n independentAmounts = {\n token: \"\"\n };\n threshold = {\n token: \"\"\n };\n minimumTransferAmount = {\n token: \"\"\n };\n rounding = {\n token: \"\"\n };\n valuationDate: string = null;\n notificationTime: string = null;\n resolutionTime: string = null;\n interestRate: Object = null;\n addressForTransfers: string = null;\n exposure: Object = null;\n localBusinessDay: Object = null;\n dailyInterestAmount: string = null;\n tradeID: string = null;\n eligibleCurrency: string;\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map index 1883d7e230..521a3d985a 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FixedLegModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FixedLegModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/model/FixedLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,mBAAc,GAAW,IAAI,CAAC;QAC9B,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,cAAS,GAAW,IAAI,CAAC;QACzB,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,eAAU,GAAW,IAAI,CAAC;QAC1B,gBAAW,GAAW,IAAI,CAAC;QAC3B,oBAAe,GAAW,IAAI,CAAC;QAC/B,6BAAwB,GAAW,IAAI,CAAC;IAC1C,CAAC;IAAD,oBAAC;AAAD,CAAC,AAhBD,IAgBC;AAhBY,qBAAa,gBAgBzB,CAAA","sourcesContent":["export class FixedLegModel {\r\n fixedRatePayer: string = null;\r\n notional = {\r\n token: \"\"\r\n };\r\n paymentFrequency: string = null;\r\n effectiveDate: string = null;\r\n terminationDate: string = null;\r\n fixedRate: Object = null;\r\n dayCountBasisDay: string = null;\r\n dayCountBasisYear: string = null;\r\n rollConvention: string = null;\r\n dayInMonth: number = null;\r\n paymentRule: string = null;\r\n paymentCalendar: string = null;\r\n interestPeriodAdjustment: string = null;\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FixedLegModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/model/FixedLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,mBAAc,GAAW,IAAI,CAAC;QAC9B,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,cAAS,GAAW,IAAI,CAAC;QACzB,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,eAAU,GAAW,IAAI,CAAC;QAC1B,gBAAW,GAAW,IAAI,CAAC;QAC3B,oBAAe,GAAW,IAAI,CAAC;QAC/B,6BAAwB,GAAW,IAAI,CAAC;IAC1C,CAAC;IAAD,oBAAC;AAAD,CAAC,AAhBD,IAgBC;AAhBY,qBAAa,gBAgBzB,CAAA","sourcesContent":["export class FixedLegModel {\n fixedRatePayer: string = null;\n notional = {\n token: \"\"\n };\n paymentFrequency: string = null;\n effectiveDate: string = null;\n terminationDate: string = null;\n fixedRate: Object = null;\n dayCountBasisDay: string = null;\n dayCountBasisYear: string = null;\n rollConvention: string = null;\n dayInMonth: number = null;\n paymentRule: string = null;\n paymentCalendar: string = null;\n interestPeriodAdjustment: string = null;\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map index f1e5bb537a..e7d7ce0933 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/model/FloatingLegModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FloatingLegModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/model/FloatingLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,sBAAiB,GAAW,IAAI,CAAC;QACjC,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,yBAAoB,GAAW,IAAI,CAAC;QACpC,eAAU,GAAW,IAAI,CAAC;QAC1B,oBAAe,GAAW,IAAI,CAAC;QAC/B,gBAAW,GAAW,IAAI,CAAC;QAC3B,iBAAY,GAAW,IAAI,CAAC;QAC5B,6BAAwB,GAAW,IAAI,CAAC;QACxC,uBAAkB,GAAW,IAAI,CAAC;QAClC,cAAS,GAAW,IAAI,CAAC;QACzB,sBAAiB,GAAW,IAAI,CAAC;QACjC,gBAAW,GAAW,IAAI,CAAC;QAC3B,UAAK,GAAW,IAAI,CAAC;QACrB,eAAU,GAAG;YACX,IAAI,EAAE,EAAE;SACT,CAAC;QACF,mBAAc,GAAa,EAAE,CAAC;QAC9B,oBAAe,GAAa,EAAE,CAAC;IACjC,CAAC;IAAD,uBAAC;AAAD,CAAC,AA3BD,IA2BC;AA3BY,wBAAgB,mBA2B5B,CAAA","sourcesContent":["export class FloatingLegModel {\r\n floatingRatePayer: string = null;\r\n notional = {\r\n token: \"\"\r\n };\r\n paymentFrequency: string = null;\r\n effectiveDate: string = null;\r\n terminationDate: string = null;\r\n dayCountBasisDay: string = null;\r\n dayCountBasisYear: string = null;\r\n rollConvention: string = null;\r\n fixingRollConvention: string = null;\r\n dayInMonth: string = null;\r\n resetDayInMonth: string = null;\r\n paymentRule: string = null;\r\n paymentDelay: string = null;\r\n interestPeriodAdjustment: string = null;\r\n fixingPeriodOffset: string = null;\r\n resetRule: string = null;\r\n fixingsPerPayment: string = null;\r\n indexSource: string = null;\r\n index: string = null;\r\n indexTenor = {\r\n name: \"\"\r\n };\r\n fixingCalendar: string[] = [];\r\n paymentCalendar: string[] = [];\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FloatingLegModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/model/FloatingLegModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,sBAAiB,GAAW,IAAI,CAAC;QACjC,aAAQ,GAAG;YACT,KAAK,EAAE,EAAE;SACV,CAAC;QACF,qBAAgB,GAAW,IAAI,CAAC;QAChC,kBAAa,GAAW,IAAI,CAAC;QAC7B,oBAAe,GAAW,IAAI,CAAC;QAC/B,qBAAgB,GAAW,IAAI,CAAC;QAChC,sBAAiB,GAAW,IAAI,CAAC;QACjC,mBAAc,GAAW,IAAI,CAAC;QAC9B,yBAAoB,GAAW,IAAI,CAAC;QACpC,eAAU,GAAW,IAAI,CAAC;QAC1B,oBAAe,GAAW,IAAI,CAAC;QAC/B,gBAAW,GAAW,IAAI,CAAC;QAC3B,iBAAY,GAAW,IAAI,CAAC;QAC5B,6BAAwB,GAAW,IAAI,CAAC;QACxC,uBAAkB,GAAW,IAAI,CAAC;QAClC,cAAS,GAAW,IAAI,CAAC;QACzB,sBAAiB,GAAW,IAAI,CAAC;QACjC,gBAAW,GAAW,IAAI,CAAC;QAC3B,UAAK,GAAW,IAAI,CAAC;QACrB,eAAU,GAAG;YACX,IAAI,EAAE,EAAE;SACT,CAAC;QACF,mBAAc,GAAa,EAAE,CAAC;QAC9B,oBAAe,GAAa,EAAE,CAAC;IACjC,CAAC;IAAD,uBAAC;AAAD,CAAC,AA3BD,IA2BC;AA3BY,wBAAgB,mBA2B5B,CAAA","sourcesContent":["export class FloatingLegModel {\n floatingRatePayer: string = null;\n notional = {\n token: \"\"\n };\n paymentFrequency: string = null;\n effectiveDate: string = null;\n terminationDate: string = null;\n dayCountBasisDay: string = null;\n dayCountBasisYear: string = null;\n rollConvention: string = null;\n fixingRollConvention: string = null;\n dayInMonth: string = null;\n resetDayInMonth: string = null;\n paymentRule: string = null;\n paymentDelay: string = null;\n interestPeriodAdjustment: string = null;\n fixingPeriodOffset: string = null;\n resetRule: string = null;\n fixingsPerPayment: string = null;\n indexSource: string = null;\n index: string = null;\n indexTenor = {\n name: \"\"\n };\n fixingCalendar: string[] = [];\n paymentCalendar: string[] = [];\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map index f2667d770b..08209cd8f0 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.js.map @@ -1 +1 @@ -{"version":3,"file":"node.service.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/node.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAG3C,IAAI,UAAU,GAAG,EAAE,CAAC;AAEpB,IAAI,IAAI,GAAG,UAAC,IAAI,EAAE,OAAO;IACvB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAC,GAAG;QACtB,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC;IACb,CAAC,EAAE,UAAC,GAAG;QACL,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AACF,qCAAmC,wBAAwB,CAAC,CAAA;AAG5D;IACE,qBAAoB,kBAAsC;QAD5D,iBAmBC;QAlBqB,uBAAkB,GAAlB,kBAAkB,CAAoB;QAE1D,sBAAiB,GAAa,UAAC,IAAI;YACjC,iEAAiE;YACjE,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C,IAAI,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACpD,MAAM,CAAI,IAAI,CAAC,WAAW,EAAE,SAAI,KAAK,SAAI,GAAK,CAAC;QACjD,CAAC,CAAC;QAEF,YAAO,GAAa,UAAC,MAAM;YACzB,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC,SAAS,EAAE,CAAC;iBACtG,IAAI,CAAC,UAAC,IAAI;gBACT,kDAAkD;gBAClD,IAAI,IAAI,GAAG,IAAI,CAAC;gBAChB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7F,MAAM,CAAC,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;IAjB2D,CAAC;IAFhE;QAAC,iBAAU,EAAE;;mBAAA;IAoBb,kBAAC;AAAD,CAAC,AAnBD,IAmBC;AAnBY,mBAAW,cAmBvB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\r\nimport { Deal } from './Deal'\r\nimport { Observable } from 'rxjs/Rx';\r\nlet curLoading = {};\r\n\r\nlet load = (type, promise) => {\r\n curLoading[type] = true;\r\n return promise.then((arg) => {\r\n curLoading[type] = false;\r\n return arg;\r\n }, (arg) => {\r\n curLoading[type] = false;\r\n throw arg;\r\n });\r\n};\r\nimport { HttpWrapperService } from './http-wrapper.service';\r\n\r\n@Injectable()\r\nexport class NodeService {\r\n constructor(private httpWrapperService: HttpWrapperService) {}\r\n\r\n formatDateForNode: Function = (date) => {\r\n // Produces yyyy-dd-mm. JS is missing proper date formatting libs\r\n let day = (\"0\" + (date.getDate())).slice(-2);\r\n let month = (\"0\" + (date.getMonth() + 1)).slice(-2);\r\n return `${date.getFullYear()}-${month}-${day}`;\r\n };\r\n\r\n getDeal: Function = (dealId) => {\r\n return load('deal' + dealId, this.httpWrapperService.getWithCounterparty('trades/' + dealId).toPromise())\r\n .then((resp) => {\r\n // Do some data modification to simplify the model\r\n let deal = resp;\r\n deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.value * 100).toString().slice(0, 6);\r\n return deal;\r\n });\r\n };\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"node.service.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/node.service.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA2B,eAAe,CAAC,CAAA;AAG3C,IAAI,UAAU,GAAG,EAAE,CAAC;AAEpB,IAAI,IAAI,GAAG,UAAC,IAAI,EAAE,OAAO;IACvB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAC,GAAG;QACtB,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC;IACb,CAAC,EAAE,UAAC,GAAG;QACL,UAAU,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACzB,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AACF,qCAAmC,wBAAwB,CAAC,CAAA;AAG5D;IACE,qBAAoB,kBAAsC;QAD5D,iBAmBC;QAlBqB,uBAAkB,GAAlB,kBAAkB,CAAoB;QAE1D,sBAAiB,GAAa,UAAC,IAAI;YACjC,iEAAiE;YACjE,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7C,IAAI,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACpD,MAAM,CAAI,IAAI,CAAC,WAAW,EAAE,SAAI,KAAK,SAAI,GAAK,CAAC;QACjD,CAAC,CAAC;QAEF,YAAO,GAAa,UAAC,MAAM;YACzB,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,EAAE,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC,SAAS,EAAE,CAAC;iBACtG,IAAI,CAAC,UAAC,IAAI;gBACT,kDAAkD;gBAClD,IAAI,IAAI,GAAG,IAAI,CAAC;gBAChB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7F,MAAM,CAAC,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;QACP,CAAC,CAAC;IAjB2D,CAAC;IAFhE;QAAC,iBAAU,EAAE;;mBAAA;IAoBb,kBAAC;AAAD,CAAC,AAnBD,IAmBC;AAnBY,mBAAW,cAmBvB,CAAA","sourcesContent":["import { Injectable } from '@angular/core';\nimport { Deal } from './Deal'\nimport { Observable } from 'rxjs/Rx';\nlet curLoading = {};\n\nlet load = (type, promise) => {\n curLoading[type] = true;\n return promise.then((arg) => {\n curLoading[type] = false;\n return arg;\n }, (arg) => {\n curLoading[type] = false;\n throw arg;\n });\n};\nimport { HttpWrapperService } from './http-wrapper.service';\n\n@Injectable()\nexport class NodeService {\n constructor(private httpWrapperService: HttpWrapperService) {}\n\n formatDateForNode: Function = (date) => {\n // Produces yyyy-dd-mm. JS is missing proper date formatting libs\n let day = (\"0\" + (date.getDate())).slice(-2);\n let month = (\"0\" + (date.getMonth() + 1)).slice(-2);\n return `${date.getFullYear()}-${month}-${day}`;\n };\n\n getDeal: Function = (dealId) => {\n return load('deal' + dealId, this.httpWrapperService.getWithCounterparty('trades/' + dealId).toPromise())\n .then((resp) => {\n // Do some data modification to simplify the model\n let deal = resp;\n deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.value * 100).toString().slice(0, 6);\n return deal;\n });\n };\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map index d51fd3a303..d667a7d3d5 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/node.service.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"node.service.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/node.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,6BAA4B,gBAAgB,CAAC,CAAA;AAE7C,QAAQ,CAAC,eAAe,EAAE;IACxB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,0BAAW,CAAC,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,0BAAW,CAAC,EAClB,UAAC,OAAoB;QACnB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { NodeService } from './node.service';\r\n\r\ndescribe('Service: Node', () => {\r\n beforeEach(() => {\r\n addProviders([NodeService]);\r\n });\r\n\r\n it('should ...',\r\n inject([NodeService],\r\n (service: NodeService) => {\r\n expect(service).toBeTruthy();\r\n }));\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"node.service.spec.js","sourceRoot":"","sources":["../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/node.service.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAEvC,wBAA4C,uBAAuB,CAAC,CAAA;AACpE,6BAA4B,gBAAgB,CAAC,CAAA;AAE7C,QAAQ,CAAC,eAAe,EAAE;IACxB,UAAU,CAAC;QACT,sBAAY,CAAC,CAAC,0BAAW,CAAC,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,YAAY,EACb,gBAAM,CAAC,CAAC,0BAAW,CAAC,EAClB,UAAC,OAAoB;QACnB,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC,CAAC,CAAC;AACV,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { NodeService } from './node.service';\n\ndescribe('Service: Node', () => {\n beforeEach(() => {\n addProviders([NodeService]);\n });\n\n it('should ...',\n inject([NodeService],\n (service: NodeService) => {\n expect(service).toBeTruthy();\n }));\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map index f2b9af6d45..dfe8afcd6b 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/portfolio/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,uBAAuB,CAAC,EAAA","sourcesContent":["export * from './portfolio.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/portfolio/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,uBAAuB,CAAC,EAAA","sourcesContent":["export * from './portfolio.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map index 3c2ce20e14..28946cbaab 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.js.map @@ -1 +1 @@ -{"version":3,"file":"portfolio.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/portfolio/portfolio.component.ts"],"names":[],"mappings":";;;;;;;;;;AAIA,2BAA2B;AAE3B,qBAAkC,eAAe,CAAC,CAAA;AAClD,8BAA+B,6BAA6B,CAAC,CAAA;AAC7D,4BAAmC,aAAa,CAAC,CAAA;AACjD,2BAAgC,+BAChC,CAAC,CAD8D,CAAC,6DAA6D;AAC7H,uBAA+C,iBAAiB,CAAC,CAAA;AACjE,8BAAsC,6BAA6B,CAAC,CAAA;AACpE,0BAAoC,qBAAqB,CAAC,CAAA;AAC1D,qCAAmC,yBAAyB,CAAC,CAAA;AAC7D,uBAAuB,iBAAiB,CAAC,CAAA;AAazC;IA6TE,4BAAoB,kBAAsC,EAAU,MAAc;QAA9D,uBAAkB,GAAlB,kBAAkB,CAAoB;QAAU,WAAM,GAAN,MAAM,CAAQ;QAtS3E,SAAI,GAAkB,EAAE,CAAC;QACzB,YAAO,GAAkB;YAC9B,8EAA8E;YAC9E,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE;YAClE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YACjF,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC9E,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC1F,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC9F,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC5F,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YACnF,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;YAClF,EAAE,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;YACnF,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;YACvE,EAAE,KAAK,EAAE,qBAAqB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;SACnG,CAAC;QACK,SAAI,GAAW,CAAC,CAAC;QACjB,iBAAY,GAAW,EAAE,CAAC;QAC1B,YAAO,GAAW,CAAC,CAAC;QACpB,aAAQ,GAAW,CAAC,CAAC;QACrB,WAAM,GAAW,CAAC,CAAC;QAEnB,WAAM,GAAQ;YACnB,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;SACnC,CAAC;QAIM,SAAI,GAAkB,EAAE,CAAC;QAEzB,iBAAY,GAAQ;YAC1B,OAAO,EAAE,aAAa;YACtB,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,CAAC;YACT,QAAQ,EAAE,CAAC;YACX,EAAE,EAAE,CAAC;YACL,GAAG,EAAE,CAAC;SACP,CAAC;IAiQmF,CAAC;IA3T9E,wCAAW,GAAnB,UAAoB,EAAE;QACpB,MAAM,CAAC,uBAAuB,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC;IAC3D,CAAC;IAEO,6CAAgB,GAAxB,UAAyB,KAAK;QAC5B,MAAM,CAAC,KAAK,CAAC;IACf,CAAC;IAEO,4CAAe,GAAvB,UAAwB,CAAC;QACvB,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACP,MAAM,CAAC,EAAE,CAAC;QACZ,CAAC;QAED,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACf,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,YAAY,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1I,IAAI,GAAG,GAAG,GAAG,CAAC;QACd,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAE1B,MAAM,CAAC,CAAC,CAAC;IACX,CAAC;IAyCO,8CAAiB,GAAzB,UAA0B,KAAK;QAC7B,IAAI,OAAO,GAAG,qCAAqC,GAAG,4BAA4B,CAAC;QAEnF,CAAC,CAAC,cAAc,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YACzC,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,IAAI;aACf;YACD,aAAa,EAAE;gBACb,QAAQ,EAAE,CAAC;aACZ;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;aAC1B;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,IAAI;iBACX;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,KAAK;oBACX,OAAO,EAAE;wBACP,WAAW,EAAE,OAAO;qBACrB;iBACF,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAEO,gDAAmB,GAA3B,UAA4B,QAAQ;QAClC,+BAA+B;QAC/B,CAAC,CAAC,gBAAgB,CAAC,CAAC,UAAU,CAAC;YAC7B,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,IAAI;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,6CAA6C;aACpD;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,EAAE;aACT;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,OAAO,EAAE,IAAI;oBACb,IAAI,EAAE,KAAK;iBACZ;gBACD,WAAW,EAAE,IAAI;gBACjB,SAAS,EAAE,IAAI;gBACf,aAAa,EAAE,IAAI;aACpB;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,IAAI;iBACX;aACF;YACD,WAAW,EAAE;gBACX,OAAO,EAAE;oBACP,MAAM,EAAE;wBACN,MAAM,EAAE,CAAC;wBACT,MAAM,EAAE;4BACN,KAAK,EAAE;gCACL,OAAO,EAAE,IAAI;gCACb,SAAS,EAAE,kBAAkB;6BAC9B;yBACF;qBACF;oBACD,MAAM,EAAE;wBACN,KAAK,EAAE;4BACL,MAAM,EAAE;gCACN,OAAO,EAAE,KAAK;6BACf;yBACF;qBACF;oBACD,OAAO,EAAE;wBACP,YAAY,EAAE,0BAA0B;wBACxC,WAAW,EAAE,qDAAqD;qBACnE;iBACF;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,QAAQ;iBACf,CAAC;SAEH,CAAC,CAAC;IACL,CAAC;IAEO,mDAAsB,GAA9B,UAA+B,MAAM,EAAE,OAAO;QAC5C,CAAC,CAAC,mBAAmB,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YAC9C,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,aAAa,EAAE;gBACb,QAAQ,EAAE,CAAC;aACZ;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;aAC1B;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,2CAA2C;aAClD;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,oBAAoB,EAAE;oBACpB,WAAW;oBACX,KAAK,EAAE,QAAQ;oBACf,IAAI,EAAE,IAAI;iBACX;gBACD,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM;iBACb;aACF;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,cAAc;iBACrB;gBACD,GAAG,EAAE,CAAC;aACP;YACD,WAAW,EAAE;gBACX,MAAM,EAAE;oBACN,MAAM,EAAE;wBACN,OAAO,EAAE,IAAI;qBACd;iBACF;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,QAAQ;iBACf,EAAE;oBACD,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,QAAQ;iBACf,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAEO,oDAAuB,GAA/B,UAAgC,MAAM;QACpC,IAAI,QAAQ,GAAG,0CAA0C;YACvD,+BAA+B,CAAC;QAElC,CAAC,CAAC,oBAAoB,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YAC/C,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,aAAa,EAAE;gBACb,QAAQ,EAAE,CAAC;aACZ;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,oBAAoB,EAAE;oBACpB,WAAW;oBACX,KAAK,EAAE,QAAQ;oBACf,IAAI,EAAE,IAAI;iBACX;gBACD,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM;iBACb;aACF;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,UAAU;iBACjB;aACF;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,eAAe;aACtB;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,eAAe;oBACrB,IAAI,EAAE,MAAM;oBACZ,OAAO,EAAE;wBACP,WAAW,EAAE,QAAQ;qBACtB;iBACF,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAEO,0DAA6B,GAArC,UAAsC,MAAM,EAAE,OAAO;QACnD,CAAC,CAAC,0BAA0B,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YACrD,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,aAAa,EAAE;gBACb,OAAO,EAAE,KAAK;aACf;YACD,SAAS,EAAE;gBACT,OAAO,EAAE,KAAK;aACf;YACD,SAAS,EAAE;gBACT,OAAO,EAAE,KAAK;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;aAC1B;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM;iBACb;aACF;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,cAAc;iBACrB;gBACD,GAAG,EAAE,CAAC;aACP;YACD,WAAW,EAAE;gBACX,MAAM,EAAE;oBACN,MAAM,EAAE;wBACN,OAAO,EAAE,IAAI;qBACd;iBACF;aACF;YACD,YAAY,EAAE;gBACZ,aAAa,EAAE,SAAS;gBACxB,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,IAAI;gBACZ,KAAK,EAAE;oBACL,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;iBACf;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,QAAQ;iBACf,EAAE;oBACD,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,QAAQ;iBACf,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAIO,oCAAO,GAAf;QAAA,iBAsEC;QArEC,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC;YAC9C,qCAAqC;YACrC,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;YAC7B,IAAI,CAAC,YAAY,CAAC,QAAQ,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,CAAC;YACzB,IAAI,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,CAAC;YAE1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,wCAAwC;YAE1D,QAAQ;YACR,IAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;gBAE1E,2BAA2B;gBAC3B,IAAI,KAAK,GAAG,EAAE,CAAC;gBAEf,4BAA4B;gBAC5B,IAAI,QAAQ,GAAG,EAAE,CAAC;gBAElB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,UAAC,KAAK,EAAE,KAAK;oBACxB,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;wBACnB,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;wBAE5D,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;oBACvC,CAAC;gBACH,CAAC,CAAC,CAAC;gBAEH,KAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;gBAE9B,KAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;gBAEnC,eAAe;gBACf,KAAI,CAAC,IAAI,GAAG,IAAI,CAAC;gBACjB,KAAI,CAAC,MAAM,GAAG,KAAI,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC/B,KAAI,CAAC,aAAa,CAAC,KAAI,CAAC,MAAM,CAAC,CAAC;YAElC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;gBACb,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;YAC7C,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC;gBAC1B,6CAA6C;gBAC7C,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,8BAA8B,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;oBAChG,gBAAgB;oBAChB,IAAI,OAAO,GAAG,IAAI,CAAA;oBAClB,KAAI,CAAC,YAAY,CAAC,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;oBAChD,KAAI,CAAC,YAAY,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;oBAC9C,KAAI,CAAC,YAAY,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC;oBAClC,KAAI,CAAC,YAAY,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;oBAEpC,IAAI,MAAM,GAAG,EAAE,CAAC;oBAChB,IAAI,OAAO,GAAG,EAAE,CAAC;oBACjB,IAAI,MAAM,GAAG,EAAE,CAAC;oBAEhB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,UAAC,KAAK,EAAE,KAAK;wBACxB,uEAAuE;wBACvE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;wBACpC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;wBACtC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;oBAChD,CAAC,CAAC,CAAC;oBAEH,KAAI,CAAC,sBAAsB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;oBAC7C,KAAI,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC;oBAErC,KAAI,CAAC,6BAA6B,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBACtD,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;oBACb,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;gBACxD,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,4CAAe,GAAvB;QAAA,iBAOC;QANC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,mBAAmB,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YAC5F,KAAI,CAAC,YAAY,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YACvC,KAAI,CAAC,YAAY,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7C,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;QACxD,CAAC,CAAC,CAAA;IACJ,CAAC;IAGD,qCAAQ,GAAR;QAAA,iBAiBC;QAhBC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YACzE,KAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACxC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,UAAU,CAAC,UAAU,CAAC;YACpB,IAAI,EAAE;gBACJ,YAAY,EAAE,GAAG;aAClB;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,SAAS,CAAC,UAAC,KAAK;YACtF,KAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,wCAAW,GAAX;QACE,IAAI,CAAC,wBAAwB,CAAC,WAAW,EAAE,CAAC;IAC9C,CAAC;IAED,yBAAyB;IAElB,uCAAU,GAAjB,UAAkB,IAAS,EAAE,IAA+B;QAA/B,oBAA+B,GAA/B,OAAsB,IAAI,CAAC,IAAI;QAC1D,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC;QAChD,IAAI,GAAG,GAAG,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QAC7E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC;IAEM,uCAAU,GAAjB,UAAkB,IAAS,EAAE,MAAW;QACtC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC;QACd,CAAC;QAED,IAAI,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;QAChD,IAAI,UAAU,GAAW,KAAK,CAAC,CAAC;QAChC,IAAI,IAAI,GAAW,KAAK,CAAC,CAAC;QAE1B,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC;gBAC3B,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC7B,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACzB,CAAC;QACH,CAAC;QAED,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC;QACd,CAAC;QAED,iBAAiB;QACjB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAC,QAAa,EAAE,OAAY;YAC3C,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAC/C,MAAM,CAAC,IAAI,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YAClC,CAAC;YAAC,IAAI,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBACtD,MAAM,CAAC,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YACjC,CAAC;YACD,MAAM,CAAC,CAAC,CAAC;QACX,CAAC,CAAC,CAAC;IACL,CAAC;IAEM,0CAAa,GAApB,UAAqB,MAAW,EAAE,IAAgE;QAAhE,oBAAgE,GAAhE,SAAc,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE;QAChG,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YACnB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD,CAAC;QAED,IAAI,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC;QACnF,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAClC,CAAC;IAjeH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,eAAe;YACzB,WAAW,EAAE,0BAA0B;YACvC,SAAS,EAAE;gBACT,yBAAyB;aAC1B;YACD,UAAU,EAAE,CAAC,gCAAkB,EAAE,8BAAc,EAAE,+BAAmB,EAAE,qCAAqB,EAAE,aAAI,EAAE,wBAAe,EAAE,4BAAe,CAAC;SACrI,CAAC;;0BAAA;IA6dF,yBAAC;AAAD,CAAC,AA3dD,IA2dC;AA3dY,0BAAkB,qBA2d9B,CAAA","sourcesContent":["// not really the Angular way:\r\n/* beautify preserve:start */\r\ndeclare var $;\r\ndeclare var Highcharts;\r\n/* beautify preserve:end */\r\n\r\nimport { Component, OnInit } from '@angular/core';\r\nimport { TAB_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\r\nimport { POPOVER_DIRECTIVES } from 'ng2-popover';\r\nimport { FORM_DIRECTIVES } from '@angular/forms/src/directives' // https://github.com/valor-software/ng2-bootstrap/issues/782\r\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\r\nimport { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\r\nimport { NG_TABLE_DIRECTIVES } from 'ng2-table/ng2-table';\r\nimport { HttpWrapperService } from '../http-wrapper.service';\r\nimport { Router } from '@angular/router';\r\nimport { Observable } from 'rxjs/Rx';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-portfolio',\r\n templateUrl: 'portfolio.component.html',\r\n styleUrls: [\r\n 'portfolio.component.css'\r\n ],\r\n directives: [POPOVER_DIRECTIVES, TAB_DIRECTIVES, NG_TABLE_DIRECTIVES, PAGINATION_DIRECTIVES, NgIf, CORE_DIRECTIVES, FORM_DIRECTIVES]\r\n})\r\n\r\nexport class PortfolioComponent implements OnInit {\r\n\r\n private IDFormatter(id) {\r\n return \"\" + id + \"\";\r\n }\r\n\r\n private defaultFormatter(value) {\r\n return value;\r\n }\r\n\r\n private numberFormatter(n) {\r\n if (!n) {\r\n return \"\";\r\n }\r\n\r\n var a = \"\" + n;\r\n a = a.replace(new RegExp(\"^(\\\\d{\" + (a.length % 3 ? a.length % 3 : 0) + \"})(\\\\d{3})\", \"g\"), \"$1 $2\").replace(/(\\d{3})+?/gi, \"$1 \").trim();\r\n var sep = \",\";\r\n a = a.replace(/\\s/g, sep);\r\n\r\n return a;\r\n }\r\n\r\n public rows: Array < any > = [];\r\n public columns: Array < any > = [\r\n // omitting the sort column on each column would result in no default sorting!\r\n { title: 'ID', name: 'id', sort: '', formatter: this.IDFormatter },\r\n { title: 'Product', name: 'product', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Type', name: 'buySell', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Trade Date', name: 'tradeDate', sort: 'desc', formatter: this.defaultFormatter },\r\n { title: 'Effective Date', name: 'effectiveDate', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Maturity Date', name: 'maturityDate', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Currency', name: 'currency', sort: '', formatter: this.defaultFormatter },\r\n { title: 'Notional', name: 'notional', sort: '', formatter: this.numberFormatter },\r\n { title: 'IM Contribution', name: 'im', sort: '', formatter: this.numberFormatter },\r\n { title: 'PV', name: 'mtm', sort: '', formatter: this.numberFormatter },\r\n { title: 'Included in summary', name: 'marginedText', sort: '', formatter: this.defaultFormatter }\r\n ];\r\n public page: number = 1;\r\n public itemsPerPage: number = 10;\r\n public maxSize: number = 5;\r\n public numPages: number = 1;\r\n public length: number = 0;\r\n\r\n public config: any = {\r\n paging: true,\r\n sorting: { columns: this.columns }\r\n };\r\n\r\n private businessDate: string;\r\n\r\n private data: Array < any > = [];\r\n\r\n private summaryTable: any = {\r\n product: \"Vanilla IRS\",\r\n currency: \"EUR\",\r\n trades: 0,\r\n notional: 0,\r\n im: 0,\r\n mtm: 0\r\n };\r\n\r\n private createTradesChart(TData) {\r\n var TFormat = 'Date: {point.x:%Y-%m-%d}
' + 'IM: {point.y:,.0f}€';\r\n\r\n $('#tradesChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n chart: {\r\n type: 'scatter',\r\n zoomType: 'xy'\r\n },\r\n rangeSelector: {\r\n selected: 4\r\n },\r\n title: {\r\n text: 'Individual Trades'\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'IM'\r\n }\r\n },\r\n series: [{\r\n name: 'Trade',\r\n data: TData,\r\n tooltip: {\r\n pointFormat: TFormat\r\n }\r\n }]\r\n });\r\n }\r\n\r\n private createIMOverVMChart(IMVMData) {\r\n // note there's no \"highstocks\"\r\n $('#IMOverVMChart').highcharts({\r\n credits: {\r\n enabled: false\r\n },\r\n chart: {\r\n type: 'scatter',\r\n zoomType: 'xy'\r\n },\r\n title: {\r\n text: 'Imminent IM over Variation Margin of trades'\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n subtitle: {\r\n text: ''\r\n },\r\n xAxis: {\r\n title: {\r\n enabled: true,\r\n text: 'MTM'\r\n },\r\n startOnTick: true,\r\n endOnTick: true,\r\n showLastLabel: true\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'IM'\r\n }\r\n },\r\n plotOptions: {\r\n scatter: {\r\n marker: {\r\n radius: 5,\r\n states: {\r\n hover: {\r\n enabled: true,\r\n lineColor: 'rgb(100,100,100)'\r\n }\r\n }\r\n },\r\n states: {\r\n hover: {\r\n marker: {\r\n enabled: false\r\n }\r\n }\r\n },\r\n tooltip: {\r\n headerFormat: '{series.name}
',\r\n pointFormat: 'IM: {point.x:,.0f}€
MTM: {point.x:,.0f}€
'\r\n }\r\n }\r\n },\r\n series: [{\r\n name: 'Trade',\r\n data: IMVMData\r\n }]\r\n\r\n });\r\n }\r\n\r\n private createIMVMHistoryChart(IMData, MTMData) {\r\n $('#IMVMHistoryChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n rangeSelector: {\r\n selected: 4\r\n },\r\n title: {\r\n text: 'Portfolio History'\r\n },\r\n subtitle: {\r\n text: 'Initial and Variation Margin Requirements'\r\n },\r\n xAxis: {\r\n type: 'datetime',\r\n dateTimeLabelFormats: { // don't display the dummy year\r\n //day: '%d'\r\n month: '%e. %b',\r\n year: '%b'\r\n },\r\n title: {\r\n text: 'Date'\r\n }\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'Exposure (€)'\r\n },\r\n min: 0\r\n },\r\n plotOptions: {\r\n spline: {\r\n marker: {\r\n enabled: true\r\n }\r\n }\r\n },\r\n series: [{\r\n name: 'Initial Margin',\r\n data: IMData,\r\n type: 'column'\r\n }, {\r\n name: 'Mark to Market',\r\n data: MTMData,\r\n type: 'spline'\r\n }]\r\n });\r\n }\r\n\r\n private createActiveTradesChart(ATData) {\r\n var ATformat = 'Active trades: {point.y:,.0f}
' +\r\n 'IM: {point.x:,.0f}
';\r\n\r\n $('#activeTradesChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n rangeSelector: {\r\n selected: 4\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n xAxis: {\r\n type: 'datetime',\r\n dateTimeLabelFormats: { // don't display the dummy year\r\n //day: '%d'\r\n month: '%e. %b',\r\n year: '%b'\r\n },\r\n title: {\r\n text: 'Date'\r\n }\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'Quantity'\r\n }\r\n },\r\n title: {\r\n text: 'Active Trades'\r\n },\r\n series: [{\r\n name: 'Active trades',\r\n data: ATData,\r\n tooltip: {\r\n pointFormat: ATformat\r\n }\r\n }]\r\n });\r\n }\r\n\r\n private createIMVMHistorySummaryChart(IMData, MTMData) {\r\n $('#IMVMHistorySummaryChart').highcharts('StockChart', {\r\n credits: {\r\n enabled: false\r\n },\r\n rangeSelector: {\r\n enabled: false\r\n },\r\n navigator: {\r\n enabled: false\r\n },\r\n scrollbar: {\r\n enabled: false\r\n },\r\n title: {\r\n text: 'Portfolio History'\r\n },\r\n legend: {\r\n enabled: true\r\n },\r\n xAxis: {\r\n type: 'datetime',\r\n title: {\r\n text: 'Date'\r\n }\r\n },\r\n yAxis: {\r\n title: {\r\n text: 'Exposure (€)'\r\n },\r\n min: 0\r\n },\r\n plotOptions: {\r\n spline: {\r\n marker: {\r\n enabled: true\r\n }\r\n }\r\n },\r\n dataGrouping: {\r\n approximation: \"average\",\r\n enabled: true,\r\n forced: true,\r\n units: [\r\n ['month', [1]]\r\n ]\r\n },\r\n series: [{\r\n name: 'Initial Margin',\r\n data: IMData,\r\n type: 'column'\r\n }, {\r\n name: 'Mark to Market',\r\n data: MTMData,\r\n type: 'spline'\r\n }]\r\n });\r\n }\r\n\r\n constructor(private httpWrapperService: HttpWrapperService, private router: Router) {}\r\n\r\n private getData() {\r\n if (this.httpWrapperService.getCounterparty()) {\r\n // re-initialize addittive table sums\r\n this.summaryTable.trades = 0;\r\n this.summaryTable.notional = 0;\r\n this.summaryTable.im = 0;\r\n this.summaryTable.mtm = 0;\r\n\r\n this.data = null; //don't leave old data in case of errors\r\n\r\n //trades\r\n this.httpWrapperService.getWithCounterparty(\"trades\").toPromise().then((data) => {\r\n\r\n // trades over time scatter\r\n var TData = [];\r\n\r\n // trades IM over VM scatter\r\n var IMVMData = [];\r\n\r\n $.each(data, (index, value) => {\r\n if (value.margined) {\r\n TData.push([new Date(value.tradeDate).getTime(), value.im]);\r\n\r\n IMVMData.push([value.im, value.mtm]);\r\n }\r\n });\r\n\r\n this.createTradesChart(TData);\r\n\r\n this.createIMOverVMChart(IMVMData);\r\n\r\n // trades table\r\n this.data = data;\r\n this.length = this.data.length;\r\n this.onChangeTable(this.config);\r\n\r\n }).catch((error) => {\r\n console.log(\"Error loading trades\", error);\r\n });\r\n\r\n this.populateSummary().then(() => {\r\n // portfolio history and active trades charts\r\n this.httpWrapperService.getWithCounterparty(\"portfolio/history/aggregated\").toPromise().then((data) => {\r\n // summary table\r\n let lastDay = data\r\n this.summaryTable.trades = lastDay.activeTrades;\r\n this.summaryTable.notional = lastDay.notional;\r\n this.summaryTable.im = lastDay.im;\r\n this.summaryTable.mtm = lastDay.mtm;\r\n\r\n var IMData = [];\r\n var MTMData = [];\r\n var ATData = [];\r\n\r\n $.each(data, (index, value) => {\r\n // new Date(value.date).getTime() when dates are switched to YYYY-MM-DD\r\n IMData.push([value.date, value.im]);\r\n MTMData.push([value.date, value.mtm]);\r\n ATData.push([value.date, value.activeTrades]);\r\n });\r\n\r\n this.createIMVMHistoryChart(IMData, MTMData);\r\n this.createActiveTradesChart(ATData);\r\n\r\n this.createIMVMHistorySummaryChart(IMData, MTMData);\r\n }).catch((error) => {\r\n console.log(\"Error loading portfolio history\", error);\r\n })\r\n })\r\n }\r\n }\r\n\r\n private populateSummary() {\r\n return this.httpWrapperService.getWithCounterparty(\"portfolio/summary\").toPromise().then((data) => {\r\n this.summaryTable.trades = data.trades;\r\n this.summaryTable.notional = data.notional;\r\n }).catch((error) => {\r\n console.log(\"Error loading portfolio summary\", error);\r\n })\r\n }\r\n\r\n counterpartySubscription: any;\r\n ngOnInit() {\r\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\r\n this.businessDate = data.businessDate;\r\n }).catch((error) => {\r\n console.log(\"Error loading business date\", error);\r\n });\r\n\r\n Highcharts.setOptions({\r\n lang: {\r\n thousandsSep: ','\r\n }\r\n });\r\n\r\n this.getData();\r\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\r\n this.getData();\r\n });\r\n }\r\n\r\n ngOnDestroy() {\r\n this.counterpartySubscription.unsubscribe();\r\n }\r\n\r\n // table helper functions\r\n\r\n public changePage(page: any, data: Array < any > = this.data): Array < any > {\r\n let start = (page.page - 1) * page.itemsPerPage;\r\n let end = page.itemsPerPage > -1 ? (start + page.itemsPerPage) : data.length;\r\n return data.slice(start, end);\r\n }\r\n\r\n public changeSort(data: any, config: any): any {\r\n if (!config.sorting) {\r\n return data;\r\n }\r\n\r\n let columns = this.config.sorting.columns || [];\r\n let columnName: string = void 0;\r\n let sort: string = void 0;\r\n\r\n for (let i = 0; i < columns.length; i++) {\r\n if (columns[i].sort !== '') {\r\n columnName = columns[i].name;\r\n sort = columns[i].sort;\r\n }\r\n }\r\n\r\n if (!columnName) {\r\n return data;\r\n }\r\n\r\n // simple sorting\r\n return data.sort((previous: any, current: any) => {\r\n if (previous[columnName] > current[columnName]) {\r\n return sort === 'desc' ? -1 : 1;\r\n } else if (previous[columnName] < current[columnName]) {\r\n return sort === 'asc' ? -1 : 1;\r\n }\r\n return 0;\r\n });\r\n }\r\n\r\n public onChangeTable(config: any, page: any = { page: this.page, itemsPerPage: this.itemsPerPage }): any {\r\n if (config.sorting) {\r\n Object.assign(this.config.sorting, config.sorting);\r\n }\r\n\r\n let sortedData = this.changeSort(this.data, this.config);\r\n this.rows = page && config.paging ? this.changePage(page, sortedData) : sortedData;\r\n this.length = sortedData.length;\r\n }\r\n\r\n // end table helper functions\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"portfolio.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/portfolio/portfolio.component.ts"],"names":[],"mappings":";;;;;;;;;;AAIA,2BAA2B;AAE3B,qBAAkC,eAAe,CAAC,CAAA;AAClD,8BAA+B,6BAA6B,CAAC,CAAA;AAC7D,4BAAmC,aAAa,CAAC,CAAA;AACjD,2BAAgC,+BAChC,CAAC,CAD8D,CAAC,6DAA6D;AAC7H,uBAA+C,iBAAiB,CAAC,CAAA;AACjE,8BAAsC,6BAA6B,CAAC,CAAA;AACpE,0BAAoC,qBAAqB,CAAC,CAAA;AAC1D,qCAAmC,yBAAyB,CAAC,CAAA;AAC7D,uBAAuB,iBAAiB,CAAC,CAAA;AAazC;IA6TE,4BAAoB,kBAAsC,EAAU,MAAc;QAA9D,uBAAkB,GAAlB,kBAAkB,CAAoB;QAAU,WAAM,GAAN,MAAM,CAAQ;QAtS3E,SAAI,GAAkB,EAAE,CAAC;QACzB,YAAO,GAAkB;YAC9B,8EAA8E;YAC9E,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE;YAClE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YACjF,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC9E,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC1F,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC9F,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YAC5F,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;YACnF,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;YAClF,EAAE,KAAK,EAAE,iBAAiB,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;YACnF,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;YACvE,EAAE,KAAK,EAAE,qBAAqB,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE;SACnG,CAAC;QACK,SAAI,GAAW,CAAC,CAAC;QACjB,iBAAY,GAAW,EAAE,CAAC;QAC1B,YAAO,GAAW,CAAC,CAAC;QACpB,aAAQ,GAAW,CAAC,CAAC;QACrB,WAAM,GAAW,CAAC,CAAC;QAEnB,WAAM,GAAQ;YACnB,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;SACnC,CAAC;QAIM,SAAI,GAAkB,EAAE,CAAC;QAEzB,iBAAY,GAAQ;YAC1B,OAAO,EAAE,aAAa;YACtB,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,CAAC;YACT,QAAQ,EAAE,CAAC;YACX,EAAE,EAAE,CAAC;YACL,GAAG,EAAE,CAAC;SACP,CAAC;IAiQmF,CAAC;IA3T9E,wCAAW,GAAnB,UAAoB,EAAE;QACpB,MAAM,CAAC,uBAAuB,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC;IAC3D,CAAC;IAEO,6CAAgB,GAAxB,UAAyB,KAAK;QAC5B,MAAM,CAAC,KAAK,CAAC;IACf,CAAC;IAEO,4CAAe,GAAvB,UAAwB,CAAC;QACvB,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACP,MAAM,CAAC,EAAE,CAAC;QACZ,CAAC;QAED,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACf,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,YAAY,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1I,IAAI,GAAG,GAAG,GAAG,CAAC;QACd,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAE1B,MAAM,CAAC,CAAC,CAAC;IACX,CAAC;IAyCO,8CAAiB,GAAzB,UAA0B,KAAK;QAC7B,IAAI,OAAO,GAAG,qCAAqC,GAAG,4BAA4B,CAAC;QAEnF,CAAC,CAAC,cAAc,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YACzC,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,IAAI;aACf;YACD,aAAa,EAAE;gBACb,QAAQ,EAAE,CAAC;aACZ;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;aAC1B;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,IAAI;iBACX;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,KAAK;oBACX,OAAO,EAAE;wBACP,WAAW,EAAE,OAAO;qBACrB;iBACF,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAEO,gDAAmB,GAA3B,UAA4B,QAAQ;QAClC,+BAA+B;QAC/B,CAAC,CAAC,gBAAgB,CAAC,CAAC,UAAU,CAAC;YAC7B,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,SAAS;gBACf,QAAQ,EAAE,IAAI;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,6CAA6C;aACpD;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,EAAE;aACT;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,OAAO,EAAE,IAAI;oBACb,IAAI,EAAE,KAAK;iBACZ;gBACD,WAAW,EAAE,IAAI;gBACjB,SAAS,EAAE,IAAI;gBACf,aAAa,EAAE,IAAI;aACpB;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,IAAI;iBACX;aACF;YACD,WAAW,EAAE;gBACX,OAAO,EAAE;oBACP,MAAM,EAAE;wBACN,MAAM,EAAE,CAAC;wBACT,MAAM,EAAE;4BACN,KAAK,EAAE;gCACL,OAAO,EAAE,IAAI;gCACb,SAAS,EAAE,kBAAkB;6BAC9B;yBACF;qBACF;oBACD,MAAM,EAAE;wBACN,KAAK,EAAE;4BACL,MAAM,EAAE;gCACN,OAAO,EAAE,KAAK;6BACf;yBACF;qBACF;oBACD,OAAO,EAAE;wBACP,YAAY,EAAE,0BAA0B;wBACxC,WAAW,EAAE,qDAAqD;qBACnE;iBACF;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,QAAQ;iBACf,CAAC;SAEH,CAAC,CAAC;IACL,CAAC;IAEO,mDAAsB,GAA9B,UAA+B,MAAM,EAAE,OAAO;QAC5C,CAAC,CAAC,mBAAmB,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YAC9C,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,aAAa,EAAE;gBACb,QAAQ,EAAE,CAAC;aACZ;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;aAC1B;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,2CAA2C;aAClD;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,oBAAoB,EAAE;oBACpB,WAAW;oBACX,KAAK,EAAE,QAAQ;oBACf,IAAI,EAAE,IAAI;iBACX;gBACD,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM;iBACb;aACF;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,cAAc;iBACrB;gBACD,GAAG,EAAE,CAAC;aACP;YACD,WAAW,EAAE;gBACX,MAAM,EAAE;oBACN,MAAM,EAAE;wBACN,OAAO,EAAE,IAAI;qBACd;iBACF;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,QAAQ;iBACf,EAAE;oBACD,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,QAAQ;iBACf,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAEO,oDAAuB,GAA/B,UAAgC,MAAM;QACpC,IAAI,QAAQ,GAAG,0CAA0C;YACvD,+BAA+B,CAAC;QAElC,CAAC,CAAC,oBAAoB,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YAC/C,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,aAAa,EAAE;gBACb,QAAQ,EAAE,CAAC;aACZ;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,oBAAoB,EAAE;oBACpB,WAAW;oBACX,KAAK,EAAE,QAAQ;oBACf,IAAI,EAAE,IAAI;iBACX;gBACD,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM;iBACb;aACF;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,UAAU;iBACjB;aACF;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,eAAe;aACtB;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,eAAe;oBACrB,IAAI,EAAE,MAAM;oBACZ,OAAO,EAAE;wBACP,WAAW,EAAE,QAAQ;qBACtB;iBACF,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAEO,0DAA6B,GAArC,UAAsC,MAAM,EAAE,OAAO;QACnD,CAAC,CAAC,0BAA0B,CAAC,CAAC,UAAU,CAAC,YAAY,EAAE;YACrD,OAAO,EAAE;gBACP,OAAO,EAAE,KAAK;aACf;YACD,aAAa,EAAE;gBACb,OAAO,EAAE,KAAK;aACf;YACD,SAAS,EAAE;gBACT,OAAO,EAAE,KAAK;aACf;YACD,SAAS,EAAE;gBACT,OAAO,EAAE,KAAK;aACf;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,mBAAmB;aAC1B;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,IAAI;aACd;YACD,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM;iBACb;aACF;YACD,KAAK,EAAE;gBACL,KAAK,EAAE;oBACL,IAAI,EAAE,cAAc;iBACrB;gBACD,GAAG,EAAE,CAAC;aACP;YACD,WAAW,EAAE;gBACX,MAAM,EAAE;oBACN,MAAM,EAAE;wBACN,OAAO,EAAE,IAAI;qBACd;iBACF;aACF;YACD,YAAY,EAAE;gBACZ,aAAa,EAAE,SAAS;gBACxB,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,IAAI;gBACZ,KAAK,EAAE;oBACL,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;iBACf;aACF;YACD,MAAM,EAAE,CAAC;oBACP,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE,QAAQ;iBACf,EAAE;oBACD,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,QAAQ;iBACf,CAAC;SACH,CAAC,CAAC;IACL,CAAC;IAIO,oCAAO,GAAf;QAAA,iBAsEC;QArEC,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC;YAC9C,qCAAqC;YACrC,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;YAC7B,IAAI,CAAC,YAAY,CAAC,QAAQ,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,YAAY,CAAC,EAAE,GAAG,CAAC,CAAC;YACzB,IAAI,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,CAAC;YAE1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,wCAAwC;YAE1D,QAAQ;YACR,IAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;gBAE1E,2BAA2B;gBAC3B,IAAI,KAAK,GAAG,EAAE,CAAC;gBAEf,4BAA4B;gBAC5B,IAAI,QAAQ,GAAG,EAAE,CAAC;gBAElB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,UAAC,KAAK,EAAE,KAAK;oBACxB,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;wBACnB,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;wBAE5D,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;oBACvC,CAAC;gBACH,CAAC,CAAC,CAAC;gBAEH,KAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;gBAE9B,KAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;gBAEnC,eAAe;gBACf,KAAI,CAAC,IAAI,GAAG,IAAI,CAAC;gBACjB,KAAI,CAAC,MAAM,GAAG,KAAI,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC/B,KAAI,CAAC,aAAa,CAAC,KAAI,CAAC,MAAM,CAAC,CAAC;YAElC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;gBACb,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;YAC7C,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC;gBAC1B,6CAA6C;gBAC7C,KAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,8BAA8B,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;oBAChG,gBAAgB;oBAChB,IAAI,OAAO,GAAG,IAAI,CAAA;oBAClB,KAAI,CAAC,YAAY,CAAC,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;oBAChD,KAAI,CAAC,YAAY,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;oBAC9C,KAAI,CAAC,YAAY,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC;oBAClC,KAAI,CAAC,YAAY,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;oBAEpC,IAAI,MAAM,GAAG,EAAE,CAAC;oBAChB,IAAI,OAAO,GAAG,EAAE,CAAC;oBACjB,IAAI,MAAM,GAAG,EAAE,CAAC;oBAEhB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,UAAC,KAAK,EAAE,KAAK;wBACxB,uEAAuE;wBACvE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;wBACpC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;wBACtC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;oBAChD,CAAC,CAAC,CAAC;oBAEH,KAAI,CAAC,sBAAsB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;oBAC7C,KAAI,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC;oBAErC,KAAI,CAAC,6BAA6B,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBACtD,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;oBACb,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;gBACxD,CAAC,CAAC,CAAA;YACJ,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,4CAAe,GAAvB;QAAA,iBAOC;QANC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,mBAAmB,CAAC,mBAAmB,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YAC5F,KAAI,CAAC,YAAY,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;YACvC,KAAI,CAAC,YAAY,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7C,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;QACxD,CAAC,CAAC,CAAA;IACJ,CAAC;IAGD,qCAAQ,GAAR;QAAA,iBAiBC;QAhBC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YACzE,KAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACxC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,UAAU,CAAC,UAAU,CAAC;YACpB,IAAI,EAAE;gBACJ,YAAY,EAAE,GAAG;aAClB;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,SAAS,CAAC,UAAC,KAAK;YACtF,KAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,wCAAW,GAAX;QACE,IAAI,CAAC,wBAAwB,CAAC,WAAW,EAAE,CAAC;IAC9C,CAAC;IAED,yBAAyB;IAElB,uCAAU,GAAjB,UAAkB,IAAS,EAAE,IAA+B;QAA/B,oBAA+B,GAA/B,OAAsB,IAAI,CAAC,IAAI;QAC1D,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC;QAChD,IAAI,GAAG,GAAG,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QAC7E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC;IAEM,uCAAU,GAAjB,UAAkB,IAAS,EAAE,MAAW;QACtC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC;QACd,CAAC;QAED,IAAI,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;QAChD,IAAI,UAAU,GAAW,KAAK,CAAC,CAAC;QAChC,IAAI,IAAI,GAAW,KAAK,CAAC,CAAC;QAE1B,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC;gBAC3B,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC7B,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACzB,CAAC;QACH,CAAC;QAED,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC;QACd,CAAC;QAED,iBAAiB;QACjB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAC,QAAa,EAAE,OAAY;YAC3C,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAC/C,MAAM,CAAC,IAAI,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YAClC,CAAC;YAAC,IAAI,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBACtD,MAAM,CAAC,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YACjC,CAAC;YACD,MAAM,CAAC,CAAC,CAAC;QACX,CAAC,CAAC,CAAC;IACL,CAAC;IAEM,0CAAa,GAApB,UAAqB,MAAW,EAAE,IAAgE;QAAhE,oBAAgE,GAAhE,SAAc,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE;QAChG,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YACnB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD,CAAC;QAED,IAAI,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI,GAAG,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,UAAU,CAAC;QACnF,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAClC,CAAC;IAjeH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,eAAe;YACzB,WAAW,EAAE,0BAA0B;YACvC,SAAS,EAAE;gBACT,yBAAyB;aAC1B;YACD,UAAU,EAAE,CAAC,gCAAkB,EAAE,8BAAc,EAAE,+BAAmB,EAAE,qCAAqB,EAAE,aAAI,EAAE,wBAAe,EAAE,4BAAe,CAAC;SACrI,CAAC;;0BAAA;IA6dF,yBAAC;AAAD,CAAC,AA3dD,IA2dC;AA3dY,0BAAkB,qBA2d9B,CAAA","sourcesContent":["// not really the Angular way:\n/* beautify preserve:start */\ndeclare var $;\ndeclare var Highcharts;\n/* beautify preserve:end */\n\nimport { Component, OnInit } from '@angular/core';\nimport { TAB_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\nimport { POPOVER_DIRECTIVES } from 'ng2-popover';\nimport { FORM_DIRECTIVES } from '@angular/forms/src/directives' // https://github.com/valor-software/ng2-bootstrap/issues/782\nimport { CORE_DIRECTIVES, NgClass, NgIf } from '@angular/common';\nimport { PAGINATION_DIRECTIVES } from 'ng2-bootstrap/ng2-bootstrap';\nimport { NG_TABLE_DIRECTIVES } from 'ng2-table/ng2-table';\nimport { HttpWrapperService } from '../http-wrapper.service';\nimport { Router } from '@angular/router';\nimport { Observable } from 'rxjs/Rx';\n\n@Component({\n moduleId: module.id,\n selector: 'app-portfolio',\n templateUrl: 'portfolio.component.html',\n styleUrls: [\n 'portfolio.component.css'\n ],\n directives: [POPOVER_DIRECTIVES, TAB_DIRECTIVES, NG_TABLE_DIRECTIVES, PAGINATION_DIRECTIVES, NgIf, CORE_DIRECTIVES, FORM_DIRECTIVES]\n})\n\nexport class PortfolioComponent implements OnInit {\n\n private IDFormatter(id) {\n return \"\" + id + \"\";\n }\n\n private defaultFormatter(value) {\n return value;\n }\n\n private numberFormatter(n) {\n if (!n) {\n return \"\";\n }\n\n var a = \"\" + n;\n a = a.replace(new RegExp(\"^(\\\\d{\" + (a.length % 3 ? a.length % 3 : 0) + \"})(\\\\d{3})\", \"g\"), \"$1 $2\").replace(/(\\d{3})+?/gi, \"$1 \").trim();\n var sep = \",\";\n a = a.replace(/\\s/g, sep);\n\n return a;\n }\n\n public rows: Array < any > = [];\n public columns: Array < any > = [\n // omitting the sort column on each column would result in no default sorting!\n { title: 'ID', name: 'id', sort: '', formatter: this.IDFormatter },\n { title: 'Product', name: 'product', sort: '', formatter: this.defaultFormatter },\n { title: 'Type', name: 'buySell', sort: '', formatter: this.defaultFormatter },\n { title: 'Trade Date', name: 'tradeDate', sort: 'desc', formatter: this.defaultFormatter },\n { title: 'Effective Date', name: 'effectiveDate', sort: '', formatter: this.defaultFormatter },\n { title: 'Maturity Date', name: 'maturityDate', sort: '', formatter: this.defaultFormatter },\n { title: 'Currency', name: 'currency', sort: '', formatter: this.defaultFormatter },\n { title: 'Notional', name: 'notional', sort: '', formatter: this.numberFormatter },\n { title: 'IM Contribution', name: 'im', sort: '', formatter: this.numberFormatter },\n { title: 'PV', name: 'mtm', sort: '', formatter: this.numberFormatter },\n { title: 'Included in summary', name: 'marginedText', sort: '', formatter: this.defaultFormatter }\n ];\n public page: number = 1;\n public itemsPerPage: number = 10;\n public maxSize: number = 5;\n public numPages: number = 1;\n public length: number = 0;\n\n public config: any = {\n paging: true,\n sorting: { columns: this.columns }\n };\n\n private businessDate: string;\n\n private data: Array < any > = [];\n\n private summaryTable: any = {\n product: \"Vanilla IRS\",\n currency: \"EUR\",\n trades: 0,\n notional: 0,\n im: 0,\n mtm: 0\n };\n\n private createTradesChart(TData) {\n var TFormat = 'Date: {point.x:%Y-%m-%d}
' + 'IM: {point.y:,.0f}€';\n\n $('#tradesChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n chart: {\n type: 'scatter',\n zoomType: 'xy'\n },\n rangeSelector: {\n selected: 4\n },\n title: {\n text: 'Individual Trades'\n },\n legend: {\n enabled: true\n },\n yAxis: {\n title: {\n text: 'IM'\n }\n },\n series: [{\n name: 'Trade',\n data: TData,\n tooltip: {\n pointFormat: TFormat\n }\n }]\n });\n }\n\n private createIMOverVMChart(IMVMData) {\n // note there's no \"highstocks\"\n $('#IMOverVMChart').highcharts({\n credits: {\n enabled: false\n },\n chart: {\n type: 'scatter',\n zoomType: 'xy'\n },\n title: {\n text: 'Imminent IM over Variation Margin of trades'\n },\n legend: {\n enabled: true\n },\n subtitle: {\n text: ''\n },\n xAxis: {\n title: {\n enabled: true,\n text: 'MTM'\n },\n startOnTick: true,\n endOnTick: true,\n showLastLabel: true\n },\n yAxis: {\n title: {\n text: 'IM'\n }\n },\n plotOptions: {\n scatter: {\n marker: {\n radius: 5,\n states: {\n hover: {\n enabled: true,\n lineColor: 'rgb(100,100,100)'\n }\n }\n },\n states: {\n hover: {\n marker: {\n enabled: false\n }\n }\n },\n tooltip: {\n headerFormat: '{series.name}
',\n pointFormat: 'IM: {point.x:,.0f}€
MTM: {point.x:,.0f}€
'\n }\n }\n },\n series: [{\n name: 'Trade',\n data: IMVMData\n }]\n\n });\n }\n\n private createIMVMHistoryChart(IMData, MTMData) {\n $('#IMVMHistoryChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n legend: {\n enabled: true\n },\n rangeSelector: {\n selected: 4\n },\n title: {\n text: 'Portfolio History'\n },\n subtitle: {\n text: 'Initial and Variation Margin Requirements'\n },\n xAxis: {\n type: 'datetime',\n dateTimeLabelFormats: { // don't display the dummy year\n //day: '%d'\n month: '%e. %b',\n year: '%b'\n },\n title: {\n text: 'Date'\n }\n },\n yAxis: {\n title: {\n text: 'Exposure (€)'\n },\n min: 0\n },\n plotOptions: {\n spline: {\n marker: {\n enabled: true\n }\n }\n },\n series: [{\n name: 'Initial Margin',\n data: IMData,\n type: 'column'\n }, {\n name: 'Mark to Market',\n data: MTMData,\n type: 'spline'\n }]\n });\n }\n\n private createActiveTradesChart(ATData) {\n var ATformat = 'Active trades: {point.y:,.0f}
' +\n 'IM: {point.x:,.0f}
';\n\n $('#activeTradesChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n rangeSelector: {\n selected: 4\n },\n legend: {\n enabled: true\n },\n xAxis: {\n type: 'datetime',\n dateTimeLabelFormats: { // don't display the dummy year\n //day: '%d'\n month: '%e. %b',\n year: '%b'\n },\n title: {\n text: 'Date'\n }\n },\n yAxis: {\n title: {\n text: 'Quantity'\n }\n },\n title: {\n text: 'Active Trades'\n },\n series: [{\n name: 'Active trades',\n data: ATData,\n tooltip: {\n pointFormat: ATformat\n }\n }]\n });\n }\n\n private createIMVMHistorySummaryChart(IMData, MTMData) {\n $('#IMVMHistorySummaryChart').highcharts('StockChart', {\n credits: {\n enabled: false\n },\n rangeSelector: {\n enabled: false\n },\n navigator: {\n enabled: false\n },\n scrollbar: {\n enabled: false\n },\n title: {\n text: 'Portfolio History'\n },\n legend: {\n enabled: true\n },\n xAxis: {\n type: 'datetime',\n title: {\n text: 'Date'\n }\n },\n yAxis: {\n title: {\n text: 'Exposure (€)'\n },\n min: 0\n },\n plotOptions: {\n spline: {\n marker: {\n enabled: true\n }\n }\n },\n dataGrouping: {\n approximation: \"average\",\n enabled: true,\n forced: true,\n units: [\n ['month', [1]]\n ]\n },\n series: [{\n name: 'Initial Margin',\n data: IMData,\n type: 'column'\n }, {\n name: 'Mark to Market',\n data: MTMData,\n type: 'spline'\n }]\n });\n }\n\n constructor(private httpWrapperService: HttpWrapperService, private router: Router) {}\n\n private getData() {\n if (this.httpWrapperService.getCounterparty()) {\n // re-initialize addittive table sums\n this.summaryTable.trades = 0;\n this.summaryTable.notional = 0;\n this.summaryTable.im = 0;\n this.summaryTable.mtm = 0;\n\n this.data = null; //don't leave old data in case of errors\n\n //trades\n this.httpWrapperService.getWithCounterparty(\"trades\").toPromise().then((data) => {\n\n // trades over time scatter\n var TData = [];\n\n // trades IM over VM scatter\n var IMVMData = [];\n\n $.each(data, (index, value) => {\n if (value.margined) {\n TData.push([new Date(value.tradeDate).getTime(), value.im]);\n\n IMVMData.push([value.im, value.mtm]);\n }\n });\n\n this.createTradesChart(TData);\n\n this.createIMOverVMChart(IMVMData);\n\n // trades table\n this.data = data;\n this.length = this.data.length;\n this.onChangeTable(this.config);\n\n }).catch((error) => {\n console.log(\"Error loading trades\", error);\n });\n\n this.populateSummary().then(() => {\n // portfolio history and active trades charts\n this.httpWrapperService.getWithCounterparty(\"portfolio/history/aggregated\").toPromise().then((data) => {\n // summary table\n let lastDay = data\n this.summaryTable.trades = lastDay.activeTrades;\n this.summaryTable.notional = lastDay.notional;\n this.summaryTable.im = lastDay.im;\n this.summaryTable.mtm = lastDay.mtm;\n\n var IMData = [];\n var MTMData = [];\n var ATData = [];\n\n $.each(data, (index, value) => {\n // new Date(value.date).getTime() when dates are switched to YYYY-MM-DD\n IMData.push([value.date, value.im]);\n MTMData.push([value.date, value.mtm]);\n ATData.push([value.date, value.activeTrades]);\n });\n\n this.createIMVMHistoryChart(IMData, MTMData);\n this.createActiveTradesChart(ATData);\n\n this.createIMVMHistorySummaryChart(IMData, MTMData);\n }).catch((error) => {\n console.log(\"Error loading portfolio history\", error);\n })\n })\n }\n }\n\n private populateSummary() {\n return this.httpWrapperService.getWithCounterparty(\"portfolio/summary\").toPromise().then((data) => {\n this.summaryTable.trades = data.trades;\n this.summaryTable.notional = data.notional;\n }).catch((error) => {\n console.log(\"Error loading portfolio summary\", error);\n })\n }\n\n counterpartySubscription: any;\n ngOnInit() {\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\n this.businessDate = data.businessDate;\n }).catch((error) => {\n console.log(\"Error loading business date\", error);\n });\n\n Highcharts.setOptions({\n lang: {\n thousandsSep: ','\n }\n });\n\n this.getData();\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\n this.getData();\n });\n }\n\n ngOnDestroy() {\n this.counterpartySubscription.unsubscribe();\n }\n\n // table helper functions\n\n public changePage(page: any, data: Array < any > = this.data): Array < any > {\n let start = (page.page - 1) * page.itemsPerPage;\n let end = page.itemsPerPage > -1 ? (start + page.itemsPerPage) : data.length;\n return data.slice(start, end);\n }\n\n public changeSort(data: any, config: any): any {\n if (!config.sorting) {\n return data;\n }\n\n let columns = this.config.sorting.columns || [];\n let columnName: string = void 0;\n let sort: string = void 0;\n\n for (let i = 0; i < columns.length; i++) {\n if (columns[i].sort !== '') {\n columnName = columns[i].name;\n sort = columns[i].sort;\n }\n }\n\n if (!columnName) {\n return data;\n }\n\n // simple sorting\n return data.sort((previous: any, current: any) => {\n if (previous[columnName] > current[columnName]) {\n return sort === 'desc' ? -1 : 1;\n } else if (previous[columnName] < current[columnName]) {\n return sort === 'asc' ? -1 : 1;\n }\n return 0;\n });\n }\n\n public onChangeTable(config: any, page: any = { page: this.page, itemsPerPage: this.itemsPerPage }): any {\n if (config.sorting) {\n Object.assign(this.config.sorting, config.sorting);\n }\n\n let sortedData = this.changeSort(this.data, this.config);\n this.rows = page && config.paging ? this.changePage(page, sortedData) : sortedData;\n this.length = sortedData.length;\n }\n\n // end table helper functions\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map index 1d494c4c57..3a1779fd31 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/portfolio/portfolio.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"portfolio.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/portfolio/portfolio.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { PortfolioComponent } from './portfolio.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"portfolio.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/portfolio/portfolio.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { PortfolioComponent } from './portfolio.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map index f3a18d4b88..296ae2a6bb 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/shared/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map index 4dbaf6f7e9..c2ce92ec00 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/valuations/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './valuations.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/valuations/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './valuations.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map index bfc06706b1..0285721250 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.js.map @@ -1 +1 @@ -{"version":3,"file":"valuations.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/valuations/valuations.component.ts"],"names":[],"mappings":";;;;;;;;;;AAEA,2BAA2B;AAE3B,qBAAkC,eAAe,CAAC,CAAA;AAClD,qCAAmC,yBAAyB,CAAC,CAAA;AAC7D,mBAA2B,SAAS,CAAC,CAAA;AASrC;IAiEE,6BAAoB,kBAAsC;QAAtC,uBAAkB,GAAlB,kBAAkB,CAAoB;QAhElD,SAAI,GAAQ,EAAE,CAAC;QACf,kBAAa,GAAQ;YAC3B,mBAAmB,EAAE,EAAE;SACxB,CAAC;QACM,aAAQ,GAAQ,EAAE,CAAC;IA4DkC,CAAC;IAnDtD,+CAAiB,GAAzB;QAAA,iBAcC;QAbC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAA;QACpC,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,2BAA2B;QAC3C,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC,wBAAwB;QAEtD,iEAAiE;QACjE,IAAI,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,gCAAgC,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,CAAE;aAC7G,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YACrB,KAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,KAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,mDAAmD;YAC1F,KAAI,CAAC,kBAAkB,CAAC,iBAAiB,EAAE,CAAC,CAAC,aAAa;YAC1D,KAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,qCAAO,GAAf;QACE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAElE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;YACzC,IAAI,CAAC,aAAa,CAAC,mBAAmB,GAAG,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAChG,CAAC;QAED,2BAA2B;QAC3B,IAAI,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;QAC1D,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;YACb,UAAU,CAAC;gBACT,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;YACrE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,sDAAsD;QACjE,CAAC;IACH,CAAC;IAED,0DAA0D;IAClD,oDAAsB,GAA9B,UAA+B,aAAa;QAC1C,IAAI,sBAAsB,GAAG,EAAE,CAAC,CAAC,yBAAyB;QAE1D,8EAA8E;QAC9E,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;YACvD,EAAE,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAC/D,IAAI,GAAG,GAAG;oBACR,KAAK,EAAE,GAAG;oBACV,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC;oBAC7D,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;iBACtE,CAAC;gBACF,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,MAAM,CAAC,sBAAsB,CAAC;IAChC,CAAC;IAID,sCAAQ,GAAR;QAAA,iBAiBC;QAhBC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YACzE,KAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACxC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,kCAAkC;QAClC,8EAA8E;QAC9E,IAAI,CAAC,KAAK,GAAG,eAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,iBAAiB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,cAAM,OAAA,KAAI,CAAC,OAAO,EAAE,EAAd,CAAc,CAAC,CAAC,CAAC;QAEtE,wDAAwD;QACxD,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,SAAS,CAAC,UAAC,KAAK;YACtF,KAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;IAEL,CAAC;IAED,yCAAW,GAAX;QACE,IAAI,CAAC,iBAAiB,CAAC,WAAW,EAAE,CAAC;QACrC,IAAI,CAAC,wBAAwB,CAAC,WAAW,EAAE,CAAC;IAC9C,CAAC;IAhGH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,2BAA2B;YACxC,SAAS,EAAE,CAAC,0BAA0B,CAAC;YACvC,UAAU,EAAE,EAAE;SACf,CAAC;;2BAAA;IA4FF,0BAAC;AAAD,CAAC,AA3FD,IA2FC;AA3FY,2BAAmB,sBA2F/B,CAAA","sourcesContent":["/* beautify preserve:start */\r\ndeclare var $;\r\n/* beautify preserve:end */\r\n\r\nimport { Component, OnInit } from '@angular/core';\r\nimport { HttpWrapperService } from '../http-wrapper.service';\r\nimport { Observable } from 'rxjs/Rx';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-valuations',\r\n templateUrl: 'valuations.component.html',\r\n styleUrls: ['valuations.component.css'],\r\n directives: []\r\n})\r\nexport class ValuationsComponent implements OnInit {\r\n private data: any = {};\r\n private formattedData: any = {\r\n sensitivitiesCurves: []\r\n };\r\n private fullData: any = {};\r\n private businessDate: string;\r\n private timer;\r\n private timerSubscription;\r\n private counterpartySubscription;\r\n\r\n // show loading spinner when clicked and data is not all received\r\n private calculateClicked: boolean;\r\n\r\n private startCalculations() {\r\n console.log(\"Starting calculations\")\r\n this.fullData = {};\r\n this.data = {}; // outdated data, delete it\r\n this.calculateClicked = true; // show loading spinners\r\n\r\n // demo magic - this is to ensure we use the right valuation date\r\n this.httpWrapperService.postWithCounterparty(\"portfolio/valuations/calculate\", { valuationDate: \"2016-06-06\" } )\r\n .toPromise().then((data) => {\r\n this.fullData = data;\r\n this.businessDate = data.businessDate; // in case it's valuations for a different date now\r\n this.httpWrapperService.startDelayedTimer(); // demo magic\r\n this.getData();\r\n });\r\n }\r\n\r\n private getData() {\r\n this.data = this.httpWrapperService.getDelayedData(this.fullData);\r\n\r\n if (this.data && this.data.sensitivities) {\r\n this.formattedData.sensitivitiesCurves = this.getSensitivitiesCurves(this.data.sensitivities);\r\n }\r\n\r\n // scroll to bottom of page\r\n let spinners = document.getElementById(\"loadingSpinners\");\r\n if (spinners) {\r\n setTimeout(() => {\r\n $(\"html, body\").animate({ scrollTop: $(document).height() }, 1000);\r\n }, 100); // wait for spinners to have gone below latest element\r\n }\r\n }\r\n\r\n // TODO: make this independent from the actual curve names\r\n private getSensitivitiesCurves(sensitivities) {\r\n let formattedSensitivities = []; // formattedSensitivities\r\n\r\n // loop on the first curve, knowing that the other curves have the same values\r\n for (let key in sensitivities.curves[\"EUR-DSCON-BIMM\"]) {\r\n if (sensitivities.curves[\"EUR-DSCON-BIMM\"].hasOwnProperty(key)) {\r\n let obj = {\r\n tenor: key, //3M, 6M etc...\r\n \"EUR-DSCON-BIMM\": sensitivities.curves[\"EUR-DSCON-BIMM\"][key],\r\n \"EUR-EURIBOR3M-BIMM\": sensitivities.curves[\"EUR-EURIBOR3M-BIMM\"][key]\r\n };\r\n formattedSensitivities.push(obj);\r\n }\r\n }\r\n\r\n return formattedSensitivities;\r\n }\r\n\r\n constructor(private httpWrapperService: HttpWrapperService) {}\r\n\r\n ngOnInit() {\r\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\r\n this.businessDate = data.businessDate;\r\n }).catch((error) => {\r\n console.log(\"Error loading business date\", error);\r\n });\r\n\r\n // check for new data periodically\r\n // higher timeout because makes debugging annoying, put to 2000 for production\r\n this.timer = Observable.timer(0, 2000);\r\n this.timerSubscription = (this.timer.subscribe(() => this.getData()));\r\n\r\n // but also check for new data when counterparty changes\r\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\r\n this.getData();\r\n });\r\n\r\n }\r\n\r\n ngOnDestroy() {\r\n this.timerSubscription.unsubscribe();\r\n this.counterpartySubscription.unsubscribe();\r\n }\r\n\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"valuations.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/valuations/valuations.component.ts"],"names":[],"mappings":";;;;;;;;;;AAEA,2BAA2B;AAE3B,qBAAkC,eAAe,CAAC,CAAA;AAClD,qCAAmC,yBAAyB,CAAC,CAAA;AAC7D,mBAA2B,SAAS,CAAC,CAAA;AASrC;IAiEE,6BAAoB,kBAAsC;QAAtC,uBAAkB,GAAlB,kBAAkB,CAAoB;QAhElD,SAAI,GAAQ,EAAE,CAAC;QACf,kBAAa,GAAQ;YAC3B,mBAAmB,EAAE,EAAE;SACxB,CAAC;QACM,aAAQ,GAAQ,EAAE,CAAC;IA4DkC,CAAC;IAnDtD,+CAAiB,GAAzB;QAAA,iBAcC;QAbC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAA;QACpC,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,2BAA2B;QAC3C,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC,wBAAwB;QAEtD,iEAAiE;QACjE,IAAI,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,gCAAgC,EAAE,EAAE,aAAa,EAAE,YAAY,EAAE,CAAE;aAC7G,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YACrB,KAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;YACrB,KAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,mDAAmD;YAC1F,KAAI,CAAC,kBAAkB,CAAC,iBAAiB,EAAE,CAAC,CAAC,aAAa;YAC1D,KAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,qCAAO,GAAf;QACE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAElE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;YACzC,IAAI,CAAC,aAAa,CAAC,mBAAmB,GAAG,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAChG,CAAC;QAED,2BAA2B;QAC3B,IAAI,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,iBAAiB,CAAC,CAAC;QAC1D,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;YACb,UAAU,CAAC;gBACT,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;YACrE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,sDAAsD;QACjE,CAAC;IACH,CAAC;IAED,0DAA0D;IAClD,oDAAsB,GAA9B,UAA+B,aAAa;QAC1C,IAAI,sBAAsB,GAAG,EAAE,CAAC,CAAC,yBAAyB;QAE1D,8EAA8E;QAC9E,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;YACvD,EAAE,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAC/D,IAAI,GAAG,GAAG;oBACR,KAAK,EAAE,GAAG;oBACV,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC;oBAC7D,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC,GAAG,CAAC;iBACtE,CAAC;gBACF,sBAAsB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QAED,MAAM,CAAC,sBAAsB,CAAC;IAChC,CAAC;IAID,sCAAQ,GAAR;QAAA,iBAiBC;QAhBC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,UAAC,IAAI;YACzE,KAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACxC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACb,OAAO,CAAC,GAAG,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,kCAAkC;QAClC,8EAA8E;QAC9E,IAAI,CAAC,KAAK,GAAG,eAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,iBAAiB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,cAAM,OAAA,KAAI,CAAC,OAAO,EAAE,EAAd,CAAc,CAAC,CAAC,CAAC;QAEtE,wDAAwD;QACxD,IAAI,CAAC,wBAAwB,GAAG,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,SAAS,CAAC,UAAC,KAAK;YACtF,KAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;IAEL,CAAC;IAED,yCAAW,GAAX;QACE,IAAI,CAAC,iBAAiB,CAAC,WAAW,EAAE,CAAC;QACrC,IAAI,CAAC,wBAAwB,CAAC,WAAW,EAAE,CAAC;IAC9C,CAAC;IAhGH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,2BAA2B;YACxC,SAAS,EAAE,CAAC,0BAA0B,CAAC;YACvC,UAAU,EAAE,EAAE;SACf,CAAC;;2BAAA;IA4FF,0BAAC;AAAD,CAAC,AA3FD,IA2FC;AA3FY,2BAAmB,sBA2F/B,CAAA","sourcesContent":["/* beautify preserve:start */\ndeclare var $;\n/* beautify preserve:end */\n\nimport { Component, OnInit } from '@angular/core';\nimport { HttpWrapperService } from '../http-wrapper.service';\nimport { Observable } from 'rxjs/Rx';\n\n@Component({\n moduleId: module.id,\n selector: 'app-valuations',\n templateUrl: 'valuations.component.html',\n styleUrls: ['valuations.component.css'],\n directives: []\n})\nexport class ValuationsComponent implements OnInit {\n private data: any = {};\n private formattedData: any = {\n sensitivitiesCurves: []\n };\n private fullData: any = {};\n private businessDate: string;\n private timer;\n private timerSubscription;\n private counterpartySubscription;\n\n // show loading spinner when clicked and data is not all received\n private calculateClicked: boolean;\n\n private startCalculations() {\n console.log(\"Starting calculations\")\n this.fullData = {};\n this.data = {}; // outdated data, delete it\n this.calculateClicked = true; // show loading spinners\n\n // demo magic - this is to ensure we use the right valuation date\n this.httpWrapperService.postWithCounterparty(\"portfolio/valuations/calculate\", { valuationDate: \"2016-06-06\" } )\n .toPromise().then((data) => {\n this.fullData = data;\n this.businessDate = data.businessDate; // in case it's valuations for a different date now\n this.httpWrapperService.startDelayedTimer(); // demo magic\n this.getData();\n });\n }\n\n private getData() {\n this.data = this.httpWrapperService.getDelayedData(this.fullData);\n\n if (this.data && this.data.sensitivities) {\n this.formattedData.sensitivitiesCurves = this.getSensitivitiesCurves(this.data.sensitivities);\n }\n\n // scroll to bottom of page\n let spinners = document.getElementById(\"loadingSpinners\");\n if (spinners) {\n setTimeout(() => {\n $(\"html, body\").animate({ scrollTop: $(document).height() }, 1000);\n }, 100); // wait for spinners to have gone below latest element\n }\n }\n\n // TODO: make this independent from the actual curve names\n private getSensitivitiesCurves(sensitivities) {\n let formattedSensitivities = []; // formattedSensitivities\n\n // loop on the first curve, knowing that the other curves have the same values\n for (let key in sensitivities.curves[\"EUR-DSCON-BIMM\"]) {\n if (sensitivities.curves[\"EUR-DSCON-BIMM\"].hasOwnProperty(key)) {\n let obj = {\n tenor: key, //3M, 6M etc...\n \"EUR-DSCON-BIMM\": sensitivities.curves[\"EUR-DSCON-BIMM\"][key],\n \"EUR-EURIBOR3M-BIMM\": sensitivities.curves[\"EUR-EURIBOR3M-BIMM\"][key]\n };\n formattedSensitivities.push(obj);\n }\n }\n\n return formattedSensitivities;\n }\n\n constructor(private httpWrapperService: HttpWrapperService) {}\n\n ngOnInit() {\n this.httpWrapperService.getAbsolute(\"business-date\").toPromise().then((data) => {\n this.businessDate = data.businessDate;\n }).catch((error) => {\n console.log(\"Error loading business date\", error);\n });\n\n // check for new data periodically\n // higher timeout because makes debugging annoying, put to 2000 for production\n this.timer = Observable.timer(0, 2000);\n this.timerSubscription = (this.timer.subscribe(() => this.getData()));\n\n // but also check for new data when counterparty changes\n this.counterpartySubscription = this.httpWrapperService.newCounterparty.subscribe((state) => {\n this.getData();\n });\n\n }\n\n ngOnDestroy() {\n this.timerSubscription.unsubscribe();\n this.counterpartySubscription.unsubscribe();\n }\n\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map index 873daaea99..4fb63fd824 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/valuations/valuations.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"valuations.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/valuations/valuations.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,uBAAuB,EAAE;IAChC,EAAE,CAAC,2BAA2B,EAAE;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { ValuationsComponent } from './valuations.component';\r\n\r\ndescribe('Component: Valuations', () => {\r\n it('should create an instance', () => {\r\n });\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"valuations.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/valuations/valuations.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,uBAAuB,EAAE;IAChC,EAAE,CAAC,2BAA2B,EAAE;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { ValuationsComponent } from './valuations.component';\n\ndescribe('Component: Valuations', () => {\n it('should create an instance', () => {\n });\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map index 8268b70ed0..2dcf1c1386 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './view-trade.component';\r\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/index.ts"],"names":[],"mappings":";;;;AAAA,iBAAc,wBAAwB,CAAC,EAAA","sourcesContent":["export * from './view-trade.component';\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map index 46e066102d..52f64b1e74 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/shared/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/shared/index.ts"],"names":[],"mappings":"","sourcesContent":[""]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html index c17db9efcc..068a5b6708 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.html @@ -27,10 +27,6 @@ Valuation Date {{deal.common.valuationDate}} - - Legal Document Hash - {{deal.common.hashLegalDocs}} - Interest Rates diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map index 2b28ce36f5..ec999b6a19 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.js.map @@ -1 +1 @@ -{"version":3,"file":"view-trade.component.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/view-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAkD,iBAAiB,CAAC,CAAA;AAUpE;IAmBE,4BAAoB,WAAwB,EAAU,KAAqB;QAAvD,gBAAW,GAAX,WAAW,CAAa;QAAU,UAAK,GAAL,KAAK,CAAgB;QAlB3E,SAAI,GAAW;YACb,QAAQ,EAAE;gBACR,QAAQ,EAAE,EAAE;gBACZ,SAAS,EAAE,EAAE;gBACb,eAAe,EAAE,EAAE;aACpB;YACD,WAAW,EAAE;gBACX,QAAQ,EAAE,EAAE;gBACZ,eAAe,EAAE,EAAE;gBACnB,cAAc,EAAE,EAAE;aACnB;YACD,MAAM,EAAE;gBACN,YAAY,EAAE;oBACZ,KAAK,EAAE,EAAE;iBACV;aACF;SACF,CAAC;IAIF,CAAC;IAED,qCAAQ,GAAR;QAAA,iBAIC;QAHC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,UAAA,MAAM,IAAI,OAAA,MAAM,CAAC,SAAS,CAAC,EAAjB,CAAiB,CAAC,CAAC,SAAS,CAAC,UAAC,OAAO;YACnE,KAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qCAAQ,GAAR,UAAS,OAAe;QAAxB,iBAQC;QAPC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC;aAC9B,IAAI,CAAC,UAAC,IAAI;YACT,KAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;aACD,KAAK,CAAC,UAAC,GAAG;YACT,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACP,CAAC;IA7CH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,2BAA2B;YACxC,SAAS,EAAE,CAAC,sBAAsB,EAAE,0BAA0B,CAAC;YAC/D,SAAS,EAAE,CAAC,0BAAW,CAAC;YACxB,UAAU,EAAE,CAAC,0BAAiB,CAAC,CAAC,2BAA2B;SAC5D,CAAC;;0BAAA;IAuCF,yBAAC;AAAD,CAAC,AAtCD,IAsCC;AAtCY,0BAAkB,qBAsC9B,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\r\nimport { NodeService } from '../node.service';\r\nimport { ROUTER_DIRECTIVES, ActivatedRoute } from '@angular/router';\r\n\r\n@Component({\r\n moduleId: module.id,\r\n selector: 'app-view-trade',\r\n templateUrl: 'view-trade.component.html',\r\n styleUrls: ['../app.component.css', 'view-trade.component.css'],\r\n providers: [NodeService],\r\n directives: [ROUTER_DIRECTIVES] // necessary for routerLink\r\n})\r\nexport class ViewTradeComponent implements OnInit {\r\n deal: Object = {\r\n fixedLeg: {\r\n notional: {},\r\n fixedRate: {},\r\n paymentCalendar: {}\r\n },\r\n floatingLeg: {\r\n notional: {},\r\n paymentCalendar: {},\r\n fixingCalendar: {}\r\n },\r\n common: {\r\n interestRate: {\r\n tenor: {}\r\n }\r\n }\r\n };\r\n\r\n constructor(private nodeService: NodeService, private route: ActivatedRoute) {\r\n\r\n }\r\n\r\n ngOnInit() {\r\n this.route.params.map(params => params['tradeId']).subscribe((tradeId) => {\r\n this.showDeal(tradeId);\r\n });\r\n }\r\n\r\n showDeal(tradeId: string) {\r\n this.nodeService.getDeal(tradeId)\r\n .then((deal) => {\r\n this.deal = deal;\r\n })\r\n .catch((err) => {\r\n console.error(err);\r\n });\r\n }\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"view-trade.component.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/view-trade.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAClD,6BAA4B,iBAAiB,CAAC,CAAA;AAC9C,uBAAkD,iBAAiB,CAAC,CAAA;AAUpE;IAmBE,4BAAoB,WAAwB,EAAU,KAAqB;QAAvD,gBAAW,GAAX,WAAW,CAAa;QAAU,UAAK,GAAL,KAAK,CAAgB;QAlB3E,SAAI,GAAW;YACb,QAAQ,EAAE;gBACR,QAAQ,EAAE,EAAE;gBACZ,SAAS,EAAE,EAAE;gBACb,eAAe,EAAE,EAAE;aACpB;YACD,WAAW,EAAE;gBACX,QAAQ,EAAE,EAAE;gBACZ,eAAe,EAAE,EAAE;gBACnB,cAAc,EAAE,EAAE;aACnB;YACD,MAAM,EAAE;gBACN,YAAY,EAAE;oBACZ,KAAK,EAAE,EAAE;iBACV;aACF;SACF,CAAC;IAIF,CAAC;IAED,qCAAQ,GAAR;QAAA,iBAIC;QAHC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,UAAA,MAAM,IAAI,OAAA,MAAM,CAAC,SAAS,CAAC,EAAjB,CAAiB,CAAC,CAAC,SAAS,CAAC,UAAC,OAAO;YACnE,KAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,qCAAQ,GAAR,UAAS,OAAe;QAAxB,iBAQC;QAPC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC;aAC9B,IAAI,CAAC,UAAC,IAAI;YACT,KAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACnB,CAAC,CAAC;aACD,KAAK,CAAC,UAAC,GAAG;YACT,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACP,CAAC;IA7CH;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,MAAM,CAAC,EAAE;YACnB,QAAQ,EAAE,gBAAgB;YAC1B,WAAW,EAAE,2BAA2B;YACxC,SAAS,EAAE,CAAC,sBAAsB,EAAE,0BAA0B,CAAC;YAC/D,SAAS,EAAE,CAAC,0BAAW,CAAC;YACxB,UAAU,EAAE,CAAC,0BAAiB,CAAC,CAAC,2BAA2B;SAC5D,CAAC;;0BAAA;IAuCF,yBAAC;AAAD,CAAC,AAtCD,IAsCC;AAtCY,0BAAkB,qBAsC9B,CAAA","sourcesContent":["import { Component, OnInit } from '@angular/core';\nimport { NodeService } from '../node.service';\nimport { ROUTER_DIRECTIVES, ActivatedRoute } from '@angular/router';\n\n@Component({\n moduleId: module.id,\n selector: 'app-view-trade',\n templateUrl: 'view-trade.component.html',\n styleUrls: ['../app.component.css', 'view-trade.component.css'],\n providers: [NodeService],\n directives: [ROUTER_DIRECTIVES] // necessary for routerLink\n})\nexport class ViewTradeComponent implements OnInit {\n deal: Object = {\n fixedLeg: {\n notional: {},\n fixedRate: {},\n paymentCalendar: {}\n },\n floatingLeg: {\n notional: {},\n paymentCalendar: {},\n fixingCalendar: {}\n },\n common: {\n interestRate: {\n tenor: {}\n }\n }\n };\n\n constructor(private nodeService: NodeService, private route: ActivatedRoute) {\n\n }\n\n ngOnInit() {\n this.route.params.map(params => params['tradeId']).subscribe((tradeId) => {\n this.showDeal(tradeId);\n });\n }\n\n showDeal(tradeId: string) {\n this.nodeService.getDeal(tradeId)\n .then((deal) => {\n this.deal = deal;\n })\n .catch((err) => {\n console.error(err);\n });\n }\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map index 464e5e8a25..e7fc9200f7 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/view-trade/view-trade.component.spec.js.map @@ -1 +1 @@ -{"version":3,"file":"view-trade.component.spec.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/view-trade/view-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,EAAE,CAAC,2BAA2B,EAAE;QAC9B,2CAA2C;QAC3C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\r\n\r\nimport { By } from '@angular/platform-browser';\r\nimport { DebugElement } from '@angular/core';\r\nimport { addProviders, async, inject } from '@angular/core/testing';\r\nimport { ViewTradeComponent } from './view-trade.component';\r\n\r\ndescribe('Component: ViewTrade', () => {\r\n it('should create an instance', () => {\r\n //let component = new ViewTradeComponent();\r\n //expect(component).toBeTruthy();\r\n });\r\n});\r\n"]} \ No newline at end of file +{"version":3,"file":"view-trade.component.spec.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/view-trade/view-trade.component.spec.ts"],"names":[],"mappings":"AAAA,uCAAuC;;AAOvC,QAAQ,CAAC,sBAAsB,EAAE;IAC/B,EAAE,CAAC,2BAA2B,EAAE;QAC9B,2CAA2C;QAC3C,iCAAiC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["/* tslint:disable:no-unused-variable */\n\nimport { By } from '@angular/platform-browser';\nimport { DebugElement } from '@angular/core';\nimport { addProviders, async, inject } from '@angular/core/testing';\nimport { ViewTradeComponent } from './view-trade.component';\n\ndescribe('Component: ViewTrade', () => {\n it('should create an instance', () => {\n //let component = new ViewTradeComponent();\n //expect(component).toBeTruthy();\n });\n});\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js index 8aef2ed8a3..abd9c9a595 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js @@ -32,7 +32,6 @@ var CommonViewModel = (function () { this.exposure = {}; this.localBusinessDay = ["London", "NewYork"]; this.dailyInterestAmount = "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360"; - this.hashLegalDocs = "put hash here"; } return CommonViewModel; }()); diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map index 59371e1e98..643a5dbefa 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/CommonViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"CommonViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/CommonViewModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAG,KAAK,CAAC;QACrB,kBAAa,GAAG,YAAY,CAAC;QAC7B,oBAAe,GAAG,YAAY,CAAC;QAC/B,0BAAqB,GAAG,8BAA8B,CAAC;QACvD,uBAAkB,GAAG;YACjB,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,cAAS,GAAG;YACR,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,0BAAqB,GAAG;YACpB,QAAQ,EAAE,QAAQ;SACrB,CAAC;QACF,aAAQ,GAAG;YACP,QAAQ,EAAE,OAAO;SACpB,CAAC;QACF,kBAAa,GAAG,0BAA0B,CAAC;QAC3C,qBAAgB,GAAG,eAAe,CAAC;QACnC,mBAAc,GAAG,mGAAmG,CAAC;QACrH,iBAAY,GAAG;YACX,MAAM,EAAE,wBAAwB;YAChC,KAAK,EAAE;gBACH,IAAI,EAAE,IAAI;aACb;YACD,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,OAAO;SAChB,CAAC;QACF,wBAAmB,GAAG,EAAE,CAAC;QACzB,aAAQ,GAAG,EAAE,CAAC;QACd,qBAAgB,GAAG,CAAE,QAAQ,EAAG,SAAS,CAAE,CAAC;QAC5C,wBAAmB,GAAG,iGAAiG,CAAC;QACxH,kBAAa,GAAG,eAAe,CAAC;IAClC,CAAC;IAAD,sBAAC;AAAD,CAAC,AAjCD,IAiCC;AAjCY,uBAAe,kBAiC3B,CAAA","sourcesContent":["export class CommonViewModel {\r\n baseCurrency = \"EUR\";\r\n effectiveDate = \"2016-02-11\";\r\n terminationDate = \"2026-02-11\";\r\n eligibleCreditSupport = \"Cash in an Eligible Currency\";\r\n independentAmounts = {\r\n quantity: 0\r\n };\r\n threshold = {\r\n quantity: 0\r\n };\r\n minimumTransferAmount = {\r\n quantity: 25000000\r\n };\r\n rounding = {\r\n quantity: 1000000\r\n };\r\n valuationDate = \"Every Local Business Day\";\r\n notificationTime = \"2:00pm London\";\r\n resolutionTime = \"2:00pm London time on the first LocalBusiness Day following the date on which the notice is given\";\r\n interestRate = {\r\n oracle: \"Rates Service Provider\",\r\n tenor: {\r\n name: \"6M\"\r\n },\r\n ratioUnit: null,\r\n name: \"EONIA\"\r\n };\r\n addressForTransfers = \"\";\r\n exposure = {};\r\n localBusinessDay = [ \"London\" , \"NewYork\" ];\r\n dailyInterestAmount = \"(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360\";\r\n hashLegalDocs = \"put hash here\";\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"CommonViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/CommonViewModel.ts"],"names":[],"mappings":";AAAA;IAAA;QACE,iBAAY,GAAG,KAAK,CAAC;QACrB,kBAAa,GAAG,YAAY,CAAC;QAC7B,oBAAe,GAAG,YAAY,CAAC;QAC/B,0BAAqB,GAAG,8BAA8B,CAAC;QACvD,uBAAkB,GAAG;YACjB,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,cAAS,GAAG;YACR,QAAQ,EAAE,CAAC;SACd,CAAC;QACF,0BAAqB,GAAG;YACpB,QAAQ,EAAE,QAAQ;SACrB,CAAC;QACF,aAAQ,GAAG;YACP,QAAQ,EAAE,OAAO;SACpB,CAAC;QACF,kBAAa,GAAG,0BAA0B,CAAC;QAC3C,qBAAgB,GAAG,eAAe,CAAC;QACnC,mBAAc,GAAG,mGAAmG,CAAC;QACrH,iBAAY,GAAG;YACX,MAAM,EAAE,wBAAwB;YAChC,KAAK,EAAE;gBACH,IAAI,EAAE,IAAI;aACb;YACD,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,OAAO;SAChB,CAAC;QACF,wBAAmB,GAAG,EAAE,CAAC;QACzB,aAAQ,GAAG,EAAE,CAAC;QACd,qBAAgB,GAAG,CAAE,QAAQ,EAAG,SAAS,CAAE,CAAC;QAC5C,wBAAmB,GAAG,iGAAiG,CAAC;IAC1H,CAAC;IAAD,sBAAC;AAAD,CAAC,AAhCD,IAgCC;AAhCY,uBAAe,kBAgC3B,CAAA","sourcesContent":["export class CommonViewModel {\n baseCurrency = \"EUR\";\n effectiveDate = \"2016-02-11\";\n terminationDate = \"2026-02-11\";\n eligibleCreditSupport = \"Cash in an Eligible Currency\";\n independentAmounts = {\n quantity: 0\n };\n threshold = {\n quantity: 0\n };\n minimumTransferAmount = {\n quantity: 25000000\n };\n rounding = {\n quantity: 1000000\n };\n valuationDate = \"Every Local Business Day\";\n notificationTime = \"2:00pm London\";\n resolutionTime = \"2:00pm London time on the first LocalBusiness Day following the date on which the notice is given\";\n interestRate = {\n oracle: \"Rates Service Provider\",\n tenor: {\n name: \"6M\"\n },\n ratioUnit: null,\n name: \"EONIA\"\n };\n addressForTransfers = \"\";\n exposure = {};\n localBusinessDay = [ \"London\" , \"NewYork\" ];\n dailyInterestAmount = \"(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360\";\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map index 629056981d..63db4778f5 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/DealViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"DealViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/DealViewModel.ts"],"names":[],"mappings":";AAAA,kCAAkC,qBAClC,CAAC,CADsD;AACvD,qCAAqC,wBACrC,CAAC,CAD4D;AAC7D,gCAAgC,mBAEhC,CAAC,CAFkD;AAEnD;IACE;QAEA,aAAQ,GAAG,IAAI,qCAAiB,EAAE,CAAC;QACnC,gBAAW,GAAG,IAAI,2CAAoB,EAAE,CAAC;QACzC,WAAM,GAAG,IAAI,iCAAe,EAAE,CAAC;IAJhB,CAAC;IAKlB,oBAAC;AAAD,CAAC,AAND,IAMC;AANY,qBAAa,gBAMzB,CAAA","sourcesContent":["import { FixedLegViewModel } from './FixedLegViewModel'\r\nimport { FloatingLegViewModel } from './FloatingLegViewModel'\r\nimport { CommonViewModel } from './CommonViewModel'\r\n\r\nexport class DealViewModel {\r\n constructor() {}\r\n\r\n fixedLeg = new FixedLegViewModel();\r\n floatingLeg = new FloatingLegViewModel();\r\n common = new CommonViewModel();\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"DealViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/DealViewModel.ts"],"names":[],"mappings":";AAAA,kCAAkC,qBAClC,CAAC,CADsD;AACvD,qCAAqC,wBACrC,CAAC,CAD4D;AAC7D,gCAAgC,mBAEhC,CAAC,CAFkD;AAEnD;IACE;QAEA,aAAQ,GAAG,IAAI,qCAAiB,EAAE,CAAC;QACnC,gBAAW,GAAG,IAAI,2CAAoB,EAAE,CAAC;QACzC,WAAM,GAAG,IAAI,iCAAe,EAAE,CAAC;IAJhB,CAAC;IAKlB,oBAAC;AAAD,CAAC,AAND,IAMC;AANY,qBAAa,gBAMzB,CAAA","sourcesContent":["import { FixedLegViewModel } from './FixedLegViewModel'\nimport { FloatingLegViewModel } from './FloatingLegViewModel'\nimport { CommonViewModel } from './CommonViewModel'\n\nexport class DealViewModel {\n constructor() {}\n\n fixedLeg = new FixedLegViewModel();\n floatingLeg = new FloatingLegViewModel();\n common = new CommonViewModel();\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js index a491a2a5a1..9ac11fba23 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js @@ -17,4 +17,4 @@ var FixedLegViewModel = (function () { return FixedLegViewModel; }()); exports.FixedLegViewModel = FixedLegViewModel; -//# sourceMappingURL=FixedLegViewModel.js.map +//# sourceMappingURL=FixedLegViewModel.js.map \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map index 0d85256f93..654543c58d 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FixedLegViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/FixedLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,mBAAc,GAAG,QAAQ,CAAC;QAC1B,aAAQ,GAAW;YACf,QAAQ,EAAE,UAAU;SACvB,CAAC;QACF,qBAAgB,GAAG,YAAY,CAAC;QAGhC,cAAS,GAAG,OAAO,CAAC;QACpB,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,eAAU,GAAW,EAAE,CAAC;QACxB,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;IAftB,CAAC;IAgBnB,wBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,yBAAiB,oBAiB7B,CAAA","sourcesContent":["export class FixedLegViewModel {\r\n constructor() { }\r\n\r\n fixedRatePayer = \"Bank A\";\r\n notional: Object = {\r\n quantity: 2500000000\r\n };\r\n paymentFrequency = \"SemiAnnual\";\r\n effectiveDateAdjustment: any;\r\n terminationDateAdjustment: any;\r\n fixedRate = \"1.676\";\r\n dayCountBasis = \"ACT/360\";\r\n rollConvention = \"ModifiedFollowing\";\r\n dayInMonth: Number = 10;\r\n paymentRule = \"InArrears\";\r\n paymentDelay = \"0\";\r\n interestPeriodAdjustment = \"Adjusted\";\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FixedLegViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/FixedLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,mBAAc,GAAG,kCAAkC,CAAC;QACpD,aAAQ,GAAW;YACf,QAAQ,EAAE,UAAU;SACvB,CAAC;QACF,qBAAgB,GAAG,YAAY,CAAC;QAGhC,cAAS,GAAG,OAAO,CAAC;QACpB,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,eAAU,GAAW,EAAE,CAAC;QACxB,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;IAftB,CAAC;IAgBnB,wBAAC;AAAD,CAAC,AAjBD,IAiBC;AAjBY,yBAAiB,oBAiB7B,CAAA","sourcesContent":["export class FixedLegViewModel {\n constructor() { }\n\n fixedRatePayer = \"CN=Bank A,O=Bank A,L=London,C=GB\";\n notional: Object = {\n quantity: 2500000000\n };\n paymentFrequency = \"SemiAnnual\";\n effectiveDateAdjustment: any;\n terminationDateAdjustment: any;\n fixedRate = \"1.676\";\n dayCountBasis = \"ACT/360\";\n rollConvention = \"ModifiedFollowing\";\n dayInMonth: Number = 10;\n paymentRule = \"InArrears\";\n paymentDelay = \"0\";\n interestPeriodAdjustment = \"Adjusted\";\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js index 1d76ebb08b..c6f59c63af 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js @@ -25,4 +25,4 @@ var FloatingLegViewModel = (function () { return FloatingLegViewModel; }()); exports.FloatingLegViewModel = FloatingLegViewModel; -//# sourceMappingURL=FloatingLegViewModel.js.map +//# sourceMappingURL=FloatingLegViewModel.js.map \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map index 7b6dc2a0f1..f97a9987be 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FloatingLegViewModel.js.map @@ -1 +1 @@ -{"version":3,"file":"FloatingLegViewModel.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/app/viewmodel/FloatingLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,sBAAiB,GAAG,QAAQ,CAAC;QAC7B,aAAQ,GAAW;YAChB,QAAQ,EAAE,UAAU;SACtB,CAAC;QACF,qBAAgB,GAAG,WAAW,CAAC;QAG/B,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,yBAAoB,GAAG,mBAAmB,CAAC;QAC3C,eAAU,GAAW,EAAE,CAAC;QACxB,oBAAe,GAAW,EAAE,CAAC;QAC7B,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;QACtC,uBAAkB,GAAW,CAAC,CAAC;QAC/B,cAAS,GAAG,WAAW,CAAC;QACxB,sBAAiB,GAAG,WAAW,CAAC;QAChC,gBAAW,GAAG,wBAAwB,CAAC;QACvC,eAAU,GAAG;YACV,IAAI,EAAE,IAAI;SACZ,CAAC;IAvBc,CAAC;IAwBnB,2BAAC;AAAD,CAAC,AAzBD,IAyBC;AAzBY,4BAAoB,uBAyBhC,CAAA","sourcesContent":["export class FloatingLegViewModel {\r\n constructor() { }\r\n\r\n floatingRatePayer = \"Bank B\";\r\n notional: Object = {\r\n quantity: 2500000000\r\n };\r\n paymentFrequency = \"Quarterly\";\r\n effectiveDateAdjustment: any;\r\n terminationDateAdjustment: any;\r\n dayCountBasis = \"ACT/360\";\r\n rollConvention = \"ModifiedFollowing\";\r\n fixingRollConvention = \"ModifiedFollowing\";\r\n dayInMonth: Number = 10;\r\n resetDayInMonth: Number = 10;\r\n paymentRule = \"InArrears\";\r\n paymentDelay = \"0\";\r\n interestPeriodAdjustment = \"Adjusted\";\r\n fixingPeriodOffset: Number = 2;\r\n resetRule = \"InAdvance\";\r\n fixingsPerPayment = \"Quarterly\";\r\n indexSource = \"Rates Service Provider\";\r\n indexTenor = {\r\n name: \"3M\"\r\n };\r\n}\r\n"]} \ No newline at end of file +{"version":3,"file":"FloatingLegViewModel.js","sourceRoot":"","sources":["../../home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/app/viewmodel/FloatingLegViewModel.ts"],"names":[],"mappings":";AAAA;IACE;QAEA,sBAAiB,GAAG,oCAAoC,CAAC;QACzD,aAAQ,GAAW;YAChB,QAAQ,EAAE,UAAU;SACtB,CAAC;QACF,qBAAgB,GAAG,WAAW,CAAC;QAG/B,kBAAa,GAAG,SAAS,CAAC;QAC1B,mBAAc,GAAG,mBAAmB,CAAC;QACrC,yBAAoB,GAAG,mBAAmB,CAAC;QAC3C,eAAU,GAAW,EAAE,CAAC;QACxB,oBAAe,GAAW,EAAE,CAAC;QAC7B,gBAAW,GAAG,WAAW,CAAC;QAC1B,iBAAY,GAAG,GAAG,CAAC;QACnB,6BAAwB,GAAG,UAAU,CAAC;QACtC,uBAAkB,GAAW,CAAC,CAAC;QAC/B,cAAS,GAAG,WAAW,CAAC;QACxB,sBAAiB,GAAG,WAAW,CAAC;QAChC,gBAAW,GAAG,wBAAwB,CAAC;QACvC,eAAU,GAAG;YACV,IAAI,EAAE,IAAI;SACZ,CAAC;IAvBc,CAAC;IAwBnB,2BAAC;AAAD,CAAC,AAzBD,IAyBC;AAzBY,4BAAoB,uBAyBhC,CAAA","sourcesContent":["export class FloatingLegViewModel {\n constructor() { }\n\n floatingRatePayer = \"CN=Bank B,O=Bank B,L=New York,C=US\";\n notional: Object = {\n quantity: 2500000000\n };\n paymentFrequency = \"Quarterly\";\n effectiveDateAdjustment: any;\n terminationDateAdjustment: any;\n dayCountBasis = \"ACT/360\";\n rollConvention = \"ModifiedFollowing\";\n fixingRollConvention = \"ModifiedFollowing\";\n dayInMonth: Number = 10;\n resetDayInMonth: Number = 10;\n paymentRule = \"InArrears\";\n paymentDelay = \"0\";\n interestPeriodAdjustment = \"Adjusted\";\n fixingPeriodOffset: Number = 2;\n resetRule = \"InAdvance\";\n fixingsPerPayment = \"Quarterly\";\n indexSource = \"Rates Service Provider\";\n indexTenor = {\n name: \"3M\"\n };\n}\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map index bb6428c60f..69b5d68d94 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/main.js.map @@ -1 +1 @@ -{"version":3,"file":"main.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/main.ts"],"names":[],"mappings":";AAAA,yCAA0B,mCAAmC,CAAC,CAAA;AAC9D,qBAA+B,eAAe,CAAC,CAAA;AAC/C,qBAA+B,eAAe,CAAC,CAAA;AAC/C,sBAAmD,gBAAgB,CAAC,CAAA;AACpE,iBAA0C,QAAQ,CAAC,CAAA;AACnD,2BAAmC,kBAAkB,CAAC,CAAA;AAEtD,EAAE,CAAC,CAAC,cAAW,CAAC,UAAU,CAAC,CAAC,CAAC;IAC3B,qBAAc,EAAE,CAAC;AACnB,CAAC;AAED,oCAAS,CAAC,eAAY,EAAE;IACtB,+BAAkB;IAClB,qBAAc;IACd,+CAA+C;IAC/C,8BAAsB,EAAE;IACxB,oBAAY,EAAE;CACf,CAAC;KACD,KAAK,CAAC,UAAA,GAAG,IAAI,OAAA,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAlB,CAAkB,CAAC,CAAC","sourcesContent":["import { bootstrap } from '@angular/platform-browser-dynamic';\r\nimport { enableProdMode } from '@angular/core';\r\nimport { HTTP_PROVIDERS } from '@angular/http';\r\nimport {disableDeprecatedForms, provideForms} from '@angular/forms';\r\nimport { AppComponent, environment } from './app/';\r\nimport { appRouterProviders } from './app/app.routes';\r\n\r\nif (environment.production) {\r\n enableProdMode();\r\n}\r\n\r\nbootstrap(AppComponent, [\r\n appRouterProviders,\r\n HTTP_PROVIDERS,\r\n // magic to fix ngModel error on ng2-bootstrap:\r\n disableDeprecatedForms(),\r\n provideForms()\r\n])\r\n.catch(err => console.error(err));\r\n"]} \ No newline at end of file +{"version":3,"file":"main.js","sourceRoot":"","sources":["home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/main.ts"],"names":[],"mappings":";AAAA,yCAA0B,mCAAmC,CAAC,CAAA;AAC9D,qBAA+B,eAAe,CAAC,CAAA;AAC/C,qBAA+B,eAAe,CAAC,CAAA;AAC/C,sBAAmD,gBAAgB,CAAC,CAAA;AACpE,iBAA0C,QAAQ,CAAC,CAAA;AACnD,2BAAmC,kBAAkB,CAAC,CAAA;AAEtD,EAAE,CAAC,CAAC,cAAW,CAAC,UAAU,CAAC,CAAC,CAAC;IAC3B,qBAAc,EAAE,CAAC;AACnB,CAAC;AAED,oCAAS,CAAC,eAAY,EAAE;IACtB,+BAAkB;IAClB,qBAAc;IACd,+CAA+C;IAC/C,8BAAsB,EAAE;IACxB,oBAAY,EAAE;CACf,CAAC;KACD,KAAK,CAAC,UAAA,GAAG,IAAI,OAAA,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAlB,CAAkB,CAAC,CAAC","sourcesContent":["import { bootstrap } from '@angular/platform-browser-dynamic';\nimport { enableProdMode } from '@angular/core';\nimport { HTTP_PROVIDERS } from '@angular/http';\nimport {disableDeprecatedForms, provideForms} from '@angular/forms';\nimport { AppComponent, environment } from './app/';\nimport { appRouterProviders } from './app/app.routes';\n\nif (environment.production) {\n enableProdMode();\n}\n\nbootstrap(AppComponent, [\n appRouterProviders,\n HTTP_PROVIDERS,\n // magic to fix ngModel error on ng2-bootstrap:\n disableDeprecatedForms(),\n provideForms()\n])\n.catch(err => console.error(err));\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map index 09a6930934..ec7e385772 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/system-config.js.map @@ -1 +1 @@ -{"version":3,"file":"system-config.js","sourceRoot":"","sources":["file:///C:/work/corda-samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-ZHoDtSjD.tmp/0/src/system-config.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,8DAA8D;AAC9D,uCAAuC;AACvC,sEAAsE;AAEtE;;gGAEgG;AAChG,kCAAkC;AAClC,IAAM,GAAG,GAAQ;IACf,QAAQ,EAAE,yBAAyB;IACnC,YAAY,EAAE,iCAAiC;IAC/C,QAAQ,EAAE,uBAAuB;IACjC,YAAY,EAAE,oCAAoC;IAClD,eAAe,EAAE,sBAAsB;IACvC,aAAa,EAAE,oBAAoB;IACnC,YAAY,EAAE,mBAAmB;IACjC,WAAW,EAAE,kBAAkB;CAChC,CAAC;AAEF,mCAAmC;AACnC,IAAM,QAAQ,GAAQ;IACpB,QAAQ,EAAE;QACR,MAAM,EAAE,KAAK;KACd;IACD,YAAY,EAAE;QACZ,MAAM,EAAE,KAAK;KACd;IACD,eAAe,EAAE;QACf,MAAM,EAAE,KAAK;QACb,gBAAgB,EAAE,IAAI;QACtB,IAAI,EAAE,kBAAkB;KACzB;IACD,aAAa,EAAE;QACb,IAAI,EAAE,UAAU;QAChB,gBAAgB,EAAE,IAAI;KACvB;IACD,YAAY,EAAE;QACZ,gBAAgB,EAAE,IAAI;KACvB;IACD,WAAW,EAAE;QACX,gBAAgB,EAAE,IAAI;KACvB;CACF,CAAC;AAEF,gGAAgG;AAChG;;gGAEgG;AAChG,IAAM,OAAO,GAAa;IACxB,4BAA4B;IAC5B,eAAe;IACf,iBAAiB;IACjB,mBAAmB;IACnB,gBAAgB;IAChB,eAAe;IACf,iBAAiB;IACjB,2BAA2B;IAC3B,mCAAmC;IAEnC,sBAAsB;IACtB,MAAM;IAEN,wBAAwB;IACxB,KAAK;IACL,YAAY;IACZ,yBAAyB;IACzB,eAAe;IACf,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;CAEjB,CAAC;AAEF,IAAM,uBAAuB,GAAQ,EAAE,CAAC;AACxC,OAAO,CAAC,OAAO,CAAC,UAAC,UAAkB;IACjC,uBAAuB,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC1D,CAAC,CAAC,CAAC;AAKH,2BAA2B;AAE3B,wCAAwC;AACxC,MAAM,CAAC,MAAM,CAAC;IACZ,OAAO,EAAE,wBAAwB;IACjC,GAAG,EAAE;QACH,UAAU,EAAE,iBAAiB;QAC7B,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,SAAS;KAClB;IACD,QAAQ,EAAE,uBAAuB;CAClC,CAAC,CAAC;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAA,GAAG,EAAE,UAAA,QAAQ,EAAE,CAAC,CAAC","sourcesContent":["\"use strict\";\r\n\r\n// SystemJS configuration file, see links for more information\r\n// https://github.com/systemjs/systemjs\r\n// https://github.com/systemjs/systemjs/blob/master/docs/config-api.md\r\n\r\n/***********************************************************************************************\r\n * User Configuration.\r\n **********************************************************************************************/\r\n/** Map relative paths to URLs. */\r\nconst map: any = {\r\n 'moment': 'vendor/moment/moment.js',\r\n 'underscore': 'vendor/underscore/underscore.js',\r\n 'jquery': 'vendor/dist/jquery.js',\r\n 'highcharts': 'vendor/highcharts/highstock.src.js',\r\n 'ng2-bootstrap': 'vendor/ng2-bootstrap',\r\n 'ng2-popover': 'vendor/ng2-popover',\r\n 'ng2-select': 'vendor/ng2-select',\r\n 'ng2-table': 'vendor/ng2-table'\r\n};\r\n\r\n/** User packages configuration. */\r\nconst packages: any = {\r\n 'moment': {\r\n format: 'cjs'\r\n },\r\n 'underscore': {\r\n format: 'cjs'\r\n },\r\n 'ng2-bootstrap': {\r\n format: 'cjs',\r\n defaultExtension: 'js',\r\n main: 'ng2-bootstrap.js'\r\n },\r\n 'ng2-popover': {\r\n main: 'index.js',\r\n defaultExtension: 'js'\r\n },\r\n 'ng2-select': {\r\n defaultExtension: 'js'\r\n },\r\n 'ng2-table': {\r\n defaultExtension: 'js'\r\n }\r\n};\r\n\r\n////////////////////////////////////////////////////////////////////////////////////////////////\r\n/***************************************httpWrapperService********************************************************\r\n * Everything underneath this line is managed by the CLI.\r\n **********************************************************************************************/\r\nconst barrels: string[] = [\r\n // Angular specific barrels.\r\n '@angular/core',\r\n '@angular/common',\r\n '@angular/compiler',\r\n '@angular/forms',\r\n '@angular/http',\r\n '@angular/router',\r\n '@angular/platform-browser',\r\n '@angular/platform-browser-dynamic',\r\n\r\n // Thirdparty barrels.\r\n 'rxjs',\r\n\r\n // App specific barrels.\r\n 'app',\r\n 'app/shared',\r\n 'app/portfolio-component',\r\n 'app/portfolio',\r\n 'app/valuations',\r\n 'app/create-trade',\r\n 'app/view-trade',\r\n /** @cli-barrel */\r\n];\r\n\r\nconst cliSystemConfigPackages: any = {};\r\nbarrels.forEach((barrelName: string) => {\r\n cliSystemConfigPackages[barrelName] = { main: 'index' };\r\n});\r\n\r\n/** Type declaration for ambient System. */\r\n/* beautify preserve:start */\r\ndeclare var System: any;\r\n/* beautify preserve:end */\r\n\r\n// Apply the CLI SystemJS configuration.\r\nSystem.config({\r\n baseURL: \"/web/simmvaluationdemo\",\r\n map: {\r\n '@angular': 'vendor/@angular',\r\n 'rxjs': 'vendor/rxjs',\r\n 'main': 'main.js'\r\n },\r\n packages: cliSystemConfigPackages\r\n});\r\n\r\n// Apply the user's configuration.\r\nSystem.config({ map, packages });\r\n"]} \ No newline at end of file +{"version":3,"file":"system-config.js","sourceRoot":"","sources":["home/arc/proj ects/corda/samples/simm-valuation-demo/src/main/web/tmp/broccoli_type_script_compiler-input_base_path-q9SObyK6.tmp/0/src/system-config.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,8DAA8D;AAC9D,uCAAuC;AACvC,sEAAsE;AAEtE;;gGAEgG;AAChG,kCAAkC;AAClC,IAAM,GAAG,GAAQ;IACf,QAAQ,EAAE,yBAAyB;IACnC,YAAY,EAAE,iCAAiC;IAC/C,QAAQ,EAAE,uBAAuB;IACjC,YAAY,EAAE,oCAAoC;IAClD,eAAe,EAAE,sBAAsB;IACvC,aAAa,EAAE,oBAAoB;IACnC,YAAY,EAAE,mBAAmB;IACjC,WAAW,EAAE,kBAAkB;CAChC,CAAC;AAEF,mCAAmC;AACnC,IAAM,QAAQ,GAAQ;IACpB,QAAQ,EAAE;QACR,MAAM,EAAE,KAAK;KACd;IACD,YAAY,EAAE;QACZ,MAAM,EAAE,KAAK;KACd;IACD,eAAe,EAAE;QACf,MAAM,EAAE,KAAK;QACb,gBAAgB,EAAE,IAAI;QACtB,IAAI,EAAE,kBAAkB;KACzB;IACD,aAAa,EAAE;QACb,IAAI,EAAE,UAAU;QAChB,gBAAgB,EAAE,IAAI;KACvB;IACD,YAAY,EAAE;QACZ,gBAAgB,EAAE,IAAI;KACvB;IACD,WAAW,EAAE;QACX,gBAAgB,EAAE,IAAI;KACvB;CACF,CAAC;AAEF,gGAAgG;AAChG;;gGAEgG;AAChG,IAAM,OAAO,GAAa;IACxB,4BAA4B;IAC5B,eAAe;IACf,iBAAiB;IACjB,mBAAmB;IACnB,gBAAgB;IAChB,eAAe;IACf,iBAAiB;IACjB,2BAA2B;IAC3B,mCAAmC;IAEnC,sBAAsB;IACtB,MAAM;IAEN,wBAAwB;IACxB,KAAK;IACL,YAAY;IACZ,yBAAyB;IACzB,eAAe;IACf,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;CAEjB,CAAC;AAEF,IAAM,uBAAuB,GAAQ,EAAE,CAAC;AACxC,OAAO,CAAC,OAAO,CAAC,UAAC,UAAkB;IACjC,uBAAuB,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC1D,CAAC,CAAC,CAAC;AAKH,2BAA2B;AAE3B,wCAAwC;AACxC,MAAM,CAAC,MAAM,CAAC;IACZ,OAAO,EAAE,wBAAwB;IACjC,GAAG,EAAE;QACH,UAAU,EAAE,iBAAiB;QAC7B,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,SAAS;KAClB;IACD,QAAQ,EAAE,uBAAuB;CAClC,CAAC,CAAC;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAA,GAAG,EAAE,UAAA,QAAQ,EAAE,CAAC,CAAC","sourcesContent":["\"use strict\";\n\n// SystemJS configuration file, see links for more information\n// https://github.com/systemjs/systemjs\n// https://github.com/systemjs/systemjs/blob/master/docs/config-api.md\n\n/***********************************************************************************************\n * User Configuration.\n **********************************************************************************************/\n/** Map relative paths to URLs. */\nconst map: any = {\n 'moment': 'vendor/moment/moment.js',\n 'underscore': 'vendor/underscore/underscore.js',\n 'jquery': 'vendor/dist/jquery.js',\n 'highcharts': 'vendor/highcharts/highstock.src.js',\n 'ng2-bootstrap': 'vendor/ng2-bootstrap',\n 'ng2-popover': 'vendor/ng2-popover',\n 'ng2-select': 'vendor/ng2-select',\n 'ng2-table': 'vendor/ng2-table'\n};\n\n/** User packages configuration. */\nconst packages: any = {\n 'moment': {\n format: 'cjs'\n },\n 'underscore': {\n format: 'cjs'\n },\n 'ng2-bootstrap': {\n format: 'cjs',\n defaultExtension: 'js',\n main: 'ng2-bootstrap.js'\n },\n 'ng2-popover': {\n main: 'index.js',\n defaultExtension: 'js'\n },\n 'ng2-select': {\n defaultExtension: 'js'\n },\n 'ng2-table': {\n defaultExtension: 'js'\n }\n};\n\n////////////////////////////////////////////////////////////////////////////////////////////////\n/***************************************httpWrapperService********************************************************\n * Everything underneath this line is managed by the CLI.\n **********************************************************************************************/\nconst barrels: string[] = [\n // Angular specific barrels.\n '@angular/core',\n '@angular/common',\n '@angular/compiler',\n '@angular/forms',\n '@angular/http',\n '@angular/router',\n '@angular/platform-browser',\n '@angular/platform-browser-dynamic',\n\n // Thirdparty barrels.\n 'rxjs',\n\n // App specific barrels.\n 'app',\n 'app/shared',\n 'app/portfolio-component',\n 'app/portfolio',\n 'app/valuations',\n 'app/create-trade',\n 'app/view-trade',\n /** @cli-barrel */\n];\n\nconst cliSystemConfigPackages: any = {};\nbarrels.forEach((barrelName: string) => {\n cliSystemConfigPackages[barrelName] = { main: 'index' };\n});\n\n/** Type declaration for ambient System. */\n/* beautify preserve:start */\ndeclare var System: any;\n/* beautify preserve:end */\n\n// Apply the CLI SystemJS configuration.\nSystem.config({\n baseURL: \"/web/simmvaluationdemo\",\n map: {\n '@angular': 'vendor/@angular',\n 'rxjs': 'vendor/rxjs',\n 'main': 'main.js'\n },\n packages: cliSystemConfigPackages\n});\n\n// Apply the user's configuration.\nSystem.config({ map, packages });\n"]} \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js index 4c5be4c0fb..644d35e274 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/vendor/jquery/dist/jquery.min.js @@ -1,4 +1,4 @@ -/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ -!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), -a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), -void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("