mirror of
https://github.com/corda/corda.git
synced 2025-01-01 02:36:44 +00:00
Merge remote-tracking branch 'github/master'
This commit is contained in:
commit
69d2d0e208
33
.gitignore
vendored
33
.gitignore
vendored
@ -10,36 +10,16 @@ tags
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
|
||||
.gradle
|
||||
local.properties
|
||||
/docs/build/doctrees
|
||||
|
||||
# General build files
|
||||
**/build/classes/**
|
||||
**/build/install/**
|
||||
**/build/kotlin-classes/**
|
||||
**/build/libs/**
|
||||
**/build/resources/**
|
||||
**/build/tmp/**
|
||||
**/build/reports/**
|
||||
**/build/jacoco/***
|
||||
**/build/test-results/**
|
||||
**/build/[0-9]*/**
|
||||
**/build/nodes/**
|
||||
**/build/scripts/**
|
||||
**/build/publications/**
|
||||
|
||||
# gradle's buildSrc build/
|
||||
/buildSrc/build/
|
||||
|
||||
# This exists only in the internal repo
|
||||
/network-explorer/build
|
||||
**/build/*
|
||||
!docs/build/*
|
||||
|
||||
lib/dokka.jar
|
||||
|
||||
buyer
|
||||
seller
|
||||
rate-fix-demo-data
|
||||
nodeA
|
||||
nodeB
|
||||
**/logs/*
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
|
||||
@ -53,8 +33,8 @@ nodeB
|
||||
.idea/inspectionProfiles
|
||||
.idea/libraries
|
||||
.idea/shelf
|
||||
|
||||
!.idea/modules.xml
|
||||
.idea/dataSources
|
||||
.idea/modules.xml
|
||||
|
||||
# if you remove the above rule, at least ignore the following:
|
||||
|
||||
@ -99,4 +79,3 @@ crashlytics-build.properties
|
||||
|
||||
# docs related
|
||||
docs/virtualenv/
|
||||
/logs/
|
||||
|
49
.idea/modules.xml
generated
49
.idea/modules.xml
generated
@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc_main.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_main.iml" group="buildSrc" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" group="buildSrc" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_integrationTest.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_main.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_main.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_test.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_test.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" group="contracts" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" group="contracts" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" group="contracts" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core.iml" group="core" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_main.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_main.iml" group="core" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/core/core_test.iml" filepath="$PROJECT_DIR$/.idea/modules/core/core_test.iml" group="core" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" group="experimental" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" group="explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" group="explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" group="explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/netPermission/netpermission.iml" filepath="$PROJECT_DIR$/.idea/modules/netPermission/netpermission.iml" group="netpermission" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/netPermission/netpermission_main.iml" filepath="$PROJECT_DIR$/.idea/modules/netPermission/netpermission_main.iml" group="netpermission" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/netPermission/netpermission_test.iml" filepath="$PROJECT_DIR$/.idea/modules/netPermission/netpermission_test.iml" group="netpermission" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_main.iml" group="network-explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-explorer_test.iml" group="network-explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-explorer/network-map-visualiser.iml" filepath="$PROJECT_DIR$/.idea/modules/network-explorer/network-map-visualiser.iml" group="network-explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator.iml" group="network-simulator" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_main.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_main.iml" group="network-simulator" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_test.iml" filepath="$PROJECT_DIR$/.idea/modules/network-simulator/network-simulator_test.iml" group="network-simulator" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" group="node" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" group="r3prototyping" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" group="test-utils" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" group="test-utils" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" group="test-utils" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
15
.idea/runConfigurations/Attachment_Demo__Run_Nodes.xml
generated
Normal file
15
.idea/runConfigurations/Attachment_Demo__Run_Nodes.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Attachment Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="attachment-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml
generated
Normal file
15
.idea/runConfigurations/Attachment_Demo__Run_Recipient.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Attachment Demo: Run Recipient" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role=RECIPIENT" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="attachment-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Attachment_Demo__Run_Sender.xml
generated
Normal file
15
.idea/runConfigurations/Attachment_Demo__Run_Sender.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Attachment Demo: Run Sender" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SENDER" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="attachment-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
3
.idea/runConfigurations/Clean___Install.xml
generated
3
.idea/runConfigurations/Clean___Install.xml
generated
@ -11,8 +11,9 @@
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="clean" />
|
||||
<option value="build" />
|
||||
<option value="installDist" />
|
||||
<option value="buildCordaJAR" />
|
||||
<option value="install" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
|
15
.idea/runConfigurations/IRS_Demo__Run_Date_Change.xml
generated
Normal file
15
.idea/runConfigurations/IRS_Demo__Run_Date_Change.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="IRS Demo: Run Date Change" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.irs.IRSDemo" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role Date 2018-01-01" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="irs-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/IRS_Demo__Run_Nodes.xml
generated
Normal file
15
.idea/runConfigurations/IRS_Demo__Run_Nodes.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="IRS Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.irs.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="irs-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/IRS_Demo__Run_Trade.xml
generated
Normal file
15
.idea/runConfigurations/IRS_Demo__Run_Trade.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="IRS Demo: Run Trade" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.irs.IRSDemo" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role Trade trade1" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="irs-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/IRS_Demo__Upload_Rates.xml
generated
Normal file
15
.idea/runConfigurations/IRS_Demo__Upload_Rates.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="IRS Demo: Upload Rates" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.irs.IRSDemo" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role UploadRates" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="irs-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
21
.idea/runConfigurations/Install.xml
generated
Normal file
21
.idea/runConfigurations/Install.xml
generated
Normal file
@ -0,0 +1,21 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Install" type="GradleRunConfiguration" factoryName="Gradle" singleton="true">
|
||||
<ExternalSystemSettings>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="installDist" />
|
||||
<option value="install" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="" />
|
||||
</ExternalSystemSettings>
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Node__buyer.xml
generated
15
.idea/runConfigurations/Node__buyer.xml
generated
@ -1,15 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Node: buyer" type="JetRunConfigurationType" factoryName="Kotlin">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="com.r3corda.demos.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar -Dco.paralleluniverse.fibers.verifyInstrumentation" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role=BUYER" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="r3prototyping_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Node__seller.xml
generated
15
.idea/runConfigurations/Node__seller.xml
generated
@ -1,15 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Node: seller" type="JetRunConfigurationType" factoryName="Kotlin">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="com.r3corda.demos.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar -Dco.paralleluniverse.fibers.verifyInstrumentation" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role=SELLER" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="r3prototyping_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Notary_Demo__Run_Nodes.xml
generated
Normal file
15
.idea/runConfigurations/Notary_Demo__Run_Nodes.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Notary Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="raft-notary-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Notary_Demo__Run_Notarisation.xml
generated
Normal file
15
.idea/runConfigurations/Notary_Demo__Run_Notarisation.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Notary Demo: Run Notarisation" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.notarydemo.NotaryDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="raft-notary-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/SIMM_Valuation_Demo.xml
generated
Normal file
15
.idea/runConfigurations/SIMM_Valuation_Demo.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="SIMM Valuation Demo" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.vega.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="simm-valuation-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
generated
Normal file
15
.idea/runConfigurations/Trader_Demo__Run_Buyer.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Trader Demo: Run Buyer" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role BUYER" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="trader-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Trader_Demo__Run_Nodes.xml
generated
Normal file
15
.idea/runConfigurations/Trader_Demo__Run_Nodes.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Trader Demo: Run Nodes" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.MainKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="trader-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
15
.idea/runConfigurations/Trader_Demo__Run_Seller.xml
generated
Normal file
15
.idea/runConfigurations/Trader_Demo__Run_Seller.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Trader Demo: Run Seller" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
|
||||
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.traderdemo.TraderDemoKt" />
|
||||
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
|
||||
<option name="PROGRAM_PARAMETERS" value="--role SELLER" />
|
||||
<option name="WORKING_DIRECTORY" value="" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" />
|
||||
<option name="PASS_PARENT_ENVS" value="true" />
|
||||
<module name="trader-demo_main" />
|
||||
<envs />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
2
.idea/runConfigurations/explorer.xml
generated
2
.idea/runConfigurations/explorer.xml
generated
@ -6,7 +6,7 @@
|
||||
<option name="LIVE_STYLESHEETS" value="false" />
|
||||
<option name="DUMP_STYLESHEETS" value="false" />
|
||||
<option name="LIVE_VIEWS" value="false" />
|
||||
<option name="MAIN_CLASS_NAME" value="com.r3corda.explorer.Main" />
|
||||
<option name="MAIN_CLASS_NAME" value="net.corda.explorer.Main" />
|
||||
<option name="VM_PARAMETERS" value="" />
|
||||
<option name="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" />
|
||||
|
52
CONTRIBUTING.md
Normal file
52
CONTRIBUTING.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Contributing to Corda
|
||||
|
||||
To start contributing you can fork our repo and begin making pull requests. Please use
|
||||
descriptive commit messages and follow our [coding style guidelines](https://docs.corda.net/codestyle.html).
|
||||
|
||||
## Community Locations
|
||||
|
||||
* [GitHub](https://github.com/corda/corda)
|
||||
* [Forums](https://discourse.corda.net)
|
||||
* [Chat](https://slack.corda.net)
|
||||
|
||||
## Developer Certificate of Origin
|
||||
|
||||
All contributions to this project are subject to the terms of the Developer Certificate of Origin, below:
|
||||
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
1 Letterman Drive
|
||||
Suite D4700
|
||||
San Francisco, CA, 94129
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
||||
Copyright 2016, R3 Limited.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
188
README.md
188
README.md
@ -1,124 +1,74 @@
|
||||
# Introduction
|
||||
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
|
||||
|
||||
This source repository contains explorations of various design concepts the R3 DLG is exploring.
|
||||
# Corda
|
||||
|
||||
Things you need to know:
|
||||
Corda is a decentralised database system in which nodes trust each other as little as possible.
|
||||
|
||||
* The main code documentation is in the form of a website in the git repository. There is a copy of the site online, so
|
||||
[access the website](http://docs.corda.r3cev.com) using the username 'corda' and password 'delegato' to start reading
|
||||
about what's included, how to get set up, and to read a tutorial on writing smart contracts in this framework.
|
||||
|
||||
* The architecture documentation is on the [Architecture Working Group Wiki](https://r3-cev.atlassian.net/wiki/display/AWG/Architecture+Working+Group) site - please
|
||||
refer to that for an explanation of some of the background concepts that the prototype is exploring.
|
||||
## Features
|
||||
|
||||
* The code is a JVM project written mostly in [Kotlin](https://kotlinlang.org/), which you can think of as a simpler
|
||||
version of Scala (or alternatively, a much better syntax for Java). Kotlin can be learned quickly and is designed
|
||||
to be readable, so you won't need to know it very well to understand what the code is doing. If you'd like to
|
||||
add new features, please read its documentation on the website.
|
||||
|
||||
There is also Java code included, to demonstrate how to use the framework from a more familiar language.
|
||||
|
||||
* For bug tracking and project management we use [JIRA](https://r3-cev.atlassian.net/secure/RapidBoard.jspa?rapidView=25&projectKey=COR).
|
||||
For source control we use this BitBucket repository. You should have received credentials for these
|
||||
services as part of getting set up. If you don't have access, please contact Richard Brown or James Carlyle.
|
||||
|
||||
* There will be a mailing list for discussion, brainstorming etc called [r3dlg-awg](https://groups.google.com/forum/#!forum/r3dlg-awg).
|
||||
|
||||
|
||||
# License
|
||||
|
||||
This code is not yet released under a traditional open source license. Until it is, the following license applies:
|
||||
|
||||
_Copyright Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
set forth therein. Distributed as Non-Project IP to R3 LRC Members pursuant to their respective Member
|
||||
and Services Agreements and subject to the Non-Project IP license terms. set forth therein. All other rights reserved._
|
||||
|
||||
# Instructions for installing prerequisite software
|
||||
|
||||
## JDK for Java 8
|
||||
|
||||
Install the Oracle JDK 8u45 or higher. It is possible that OpenJDK will also work but we have not tested with this.
|
||||
|
||||
## Using IntelliJ
|
||||
|
||||
It's a good idea to use a modern IDE. We use IntelliJ. Install the __latest version__ of IntelliJ community edition (which is free):
|
||||
|
||||
https://www.jetbrains.com/idea/download/
|
||||
|
||||
Upgrade the Kotlin plugin to the latest version (1.0-beta-2423) by clicking "Configure > Plugins" in the opening screen,
|
||||
then clicking "Install JetBrains plugin", then searching for Kotlin, then hitting "Upgrade" and then "Restart".
|
||||
|
||||
Choose "Check out from version control" and use this git URL
|
||||
|
||||
https://your_username@bitbucket.org/R3-CEV/r3prototyping.git
|
||||
|
||||
Agree to the defaults for importing a Gradle project. Wait for it to download the dependencies.
|
||||
|
||||
Right click on the tests directory, click "Run -> All Tests" (note: NOT the first item in the submenu that has the gradle logo next to it).
|
||||
|
||||
The code should build, the unit tests should show as all green.
|
||||
|
||||
You can catch up with the latest code by selecting "VCS -> Update Project" in the menu.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## IntelliJ
|
||||
|
||||
If on attempting to open the project, IntelliJ refuses because SDK was not selected, do the following:
|
||||
|
||||
Configure -> Project Defaults -> Project Structure
|
||||
|
||||
on that tab:
|
||||
|
||||
Project Settings / Project
|
||||
|
||||
click on New… next to the red <No SDK> symbol, and select JDK. It should then pop up and show the latest JDK it has found at something like
|
||||
|
||||
jdk1.8.0_xx…/Contents/Home
|
||||
|
||||
Also select Project language level: as 8. Click OK. Open should now work.
|
||||
|
||||
## Quasar
|
||||
|
||||
If you get an error about a missing Quasar agent, then your JVM is being invoked without a needed command line argument.
|
||||
Make sure an argument like `-javaagent:lib/quasar.jar` is being passed to the invocation.
|
||||
|
||||
You may need/want to edit your default JUnit run config in IntelliJ to ensure that parameter is being set, along with
|
||||
`-Dco.paralleluniverse.fibers.verifyInstrumentation` which is useful to catch mistakes. To do that, click the dropdown
|
||||
in the toolbar and select "Edit configurations", then expand the defaults tree, then select JUnit and add the two
|
||||
arguments to the VM options edit.
|
||||
|
||||
## "Foo is ambiguous" error during compilation
|
||||
|
||||
Gradle's incremental compilation isn't always reliable. Run `./gradlew clean` and then try again.
|
||||
|
||||
## ClassNotFoundException during Gradle quasarScan task
|
||||
|
||||
Your Gradle build server is hosed. Run `gradle --stop` and then try `gradle clean build`.
|
||||
|
||||
# Accessing Source Without an IDE
|
||||
|
||||
If you don't want to explore or modify the code in a local IDE, you can also just use the command line and a text editor:
|
||||
|
||||
git clone https://your_username@bitbucket.org/R3-CEV/r3prototyping.git
|
||||
|
||||
You will need to have your Bitbucket account set up already from R3. Then:
|
||||
|
||||
cd r3prototyping
|
||||
|
||||
Run the following to run the unit tests:
|
||||
|
||||
./gradlew test
|
||||
|
||||
For the first time only, this will download and configure Gradle.
|
||||
Run "git pull" to upgrade
|
||||
|
||||
# Starting point - the Tutorial
|
||||
|
||||
We have prepared a comprehensive tutorial.
|
||||
One you have access to the source, open the following in a browser:
|
||||
|
||||
r3prototyping/docs/build/html/index.html
|
||||
* A P2P network of nodes
|
||||
* Smart contracts
|
||||
* Flow framework
|
||||
* "Notary" infrastructure to validate uniqueness of transactions
|
||||
* Written as a platform for distributed apps called CorDapps
|
||||
* Written in [Kotlin](https://kotlinlang.org), targeting the JVM
|
||||
|
||||
![Screenshot](https://r3-cev.atlassian.net/wiki/download/attachments/3441064/Screen%20Shot%202015-12-10%20at%2010.43.06.png)
|
||||
Read our full and planned feature list [here](https://docs.corda.net/inthebox.html).
|
||||
|
||||
## Getting started
|
||||
|
||||
Firstly, read the [Getting started](https://docs.corda.net/getting-set-up.html) documentation.
|
||||
|
||||
Watching the following webinars will give you a great introduction to Corda:
|
||||
|
||||
### Webinar 1 – [Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1)
|
||||
|
||||
Richard Brown, R3 Chief Technology Officer, explains Corda's unique architecture, the only distributed ledger platform designed by and for the financial industry's unique requirements. You may want to read the [Corda non-technical whitepaper](https://www.r3.com/s/corda-introductory-whitepaper-final.pdf) as pre-reading for this session.
|
||||
|
||||
### Webinar 2 – [Corda Developers’ Tutorial](https://vimeo.com/192797322/aab499b152)
|
||||
|
||||
Roger Willis, R3 Developer Relations Lead, provides an overview of Corda from a developer’s perspective and guidance on how to start building CorDapps. You may want to view [Webinar 1 - Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1) as preparation for this session. **NB. This was recorded for the M5 release.**
|
||||
|
||||
## Building on Corda
|
||||
|
||||
To build your own CorDapps:
|
||||
|
||||
1. Clone the [CorDapp Template repository](https://github.com/corda/cordapp-template)
|
||||
2. Read the [README](https://github.com/corda/cordapp-template/blob/master/README.md) (**IMPORTANT!**)
|
||||
3. Read the [Writing a CorDapp](https://docs.corda.net/tutorial-cordapp.html) documentation
|
||||
|
||||
To look at the Corda source and run some sample applications:
|
||||
|
||||
1. Clone this repository
|
||||
2. To run some sample CorDapps, read the [running the demos documentation](https://docs.corda.r3cev.com/running-the-demos.html)
|
||||
3. Start hacking and [contribute](./CONTRIBUTING.md)!
|
||||
|
||||
## Useful links
|
||||
|
||||
* [Project website](https://corda.net)
|
||||
* [Documentation](https://docs.corda.net)
|
||||
* [Slack channel] (https://slack.corda.net/)
|
||||
* [Forum](https://discourse.corda.net)
|
||||
|
||||
## Development State
|
||||
|
||||
Corda is currently in very early development and should not be used in production systems. Breaking
|
||||
changes will happen on minor versions until 1.0. Experimentation with Corda is recommended.
|
||||
|
||||
Pull requests, experiments, and contributions are encouraged and welcomed.
|
||||
|
||||
## Background
|
||||
|
||||
The project is supported by R3, a financial industry consortium, which is why it
|
||||
contains some code for financial use cases and why the documentation focuses on finance. The goal is to use it
|
||||
to construct a global ledger, simplifying finance and reducing the overheads of banking. But it is run as
|
||||
an open source project and the basic technology of a peer-to-peer decentralised database may be useful
|
||||
for many different projects.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read [here](./CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](./LICENCE)
|
||||
|
4
TRADEMARK
Normal file
4
TRADEMARK
Normal file
@ -0,0 +1,4 @@
|
||||
Corda and the Corda logo are trademarks of R3CEV LLC and its affiliates.
|
||||
All rights reserved.
|
||||
|
||||
For R3CEV LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-usage-policy
|
237
build.gradle
237
build.gradle
@ -1,22 +1,35 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.0.3'
|
||||
// Our version: bump this on release.
|
||||
ext.corda_version = "0.7-SNAPSHOT"
|
||||
ext.gradle_plugins_version = "0.6.1"
|
||||
|
||||
ext.kotlin_version = '1.0.5'
|
||||
ext.quasar_version = '0.7.6'
|
||||
ext.asm_version = '0.5.3'
|
||||
ext.artemis_version = '1.4.0'
|
||||
ext.jackson_version = '2.8.0.rc2'
|
||||
ext.jetty_version = '9.3.9.v20160517'
|
||||
ext.jersey_version = '2.23.1'
|
||||
ext.jolokia_version = '2.0.0-M1'
|
||||
ext.jolokia_version = '2.0.0-M1'
|
||||
ext.assertj_version = '3.5.1'
|
||||
ext.log4j_version = '2.6.2'
|
||||
ext.bouncycastle_version = '1.54'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
// TODO: Remove this once all packages are published to jcenter or maven central. (M6 or 7).
|
||||
maven {
|
||||
url "http://r3.bintray.com/corda"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4'
|
||||
classpath "net.corda.plugins:publish-utils:$gradle_plugins_version"
|
||||
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
|
||||
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
|
||||
|
||||
// Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0'
|
||||
@ -30,11 +43,20 @@ plugins {
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'project-report'
|
||||
apply plugin: QuasarPlugin
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
|
||||
// We need the following three lines even though they're inside an allprojects {} block below because otherwise
|
||||
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
|
||||
// with the run configurations. It also doesn't realise that the project is a Java 8 project and misconfigures
|
||||
// the resulting import. This fixes it.
|
||||
apply plugin: 'java'
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'java'
|
||||
@ -44,118 +66,36 @@ allprojects {
|
||||
targetCompatibility = 1.8
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
|
||||
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options"
|
||||
}
|
||||
|
||||
// Our version: bump this on release.
|
||||
group 'com.r3corda'
|
||||
version '0.5-SNAPSHOT'
|
||||
group 'net.corda'
|
||||
version "$corda_version"
|
||||
}
|
||||
|
||||
// Check that we are running on a Java 8 JDK. The source/targetCompatibility values above aren't sufficient to
|
||||
// guarantee this because those are properties checked by the Java plugin, but we're using Kotlin.
|
||||
//
|
||||
// We recommend a specific minor version (unfortunately, not checkable directly) because JavaFX adds APIs in
|
||||
// minor releases, so we can't work with just any Java 8, it has to be a recent one.
|
||||
if (!JavaVersion.current().java8Compatible)
|
||||
throw new GradleException("Corda requires Java 8, please upgrade to at least 1.8.0_112")
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
main {
|
||||
resources {
|
||||
srcDir "config/dev"
|
||||
}
|
||||
}
|
||||
test {
|
||||
resources {
|
||||
srcDir "config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
configurations {
|
||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
||||
runtime.exclude module: 'isolated'
|
||||
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
// This is required for quasar. I think.
|
||||
applicationDefaultJvmArgs = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
// Needed by the :startScripts task
|
||||
mainClassName = 'com.r3corda.demos.TraderDemoKt'
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||
|
||||
// Required for building out the fat JAR.
|
||||
dependencies {
|
||||
compile project(':node')
|
||||
// TODO: Demos should not depend on test code, but only use production APIs
|
||||
compile project(':test-utils')
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.assertj:assertj-core:3.4.1'
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile 'junit:junit:4.12'
|
||||
integrationTestCompile 'org.assertj:assertj-core:${assertj_version}'
|
||||
integrationTestCompile project(':test-utils')
|
||||
compile "com.google.guava:guava:19.0"
|
||||
runtime project(path: ":node", configuration: 'runtimeArtifacts')
|
||||
}
|
||||
|
||||
// Package up the demo programs.
|
||||
|
||||
task getAttachmentDemo(type: CreateStartScripts) {
|
||||
mainClassName = "com.r3corda.demos.attachment.AttachmentDemoKt"
|
||||
applicationName = "attachment-demo"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
task getTraderDemo(type: CreateStartScripts) {
|
||||
mainClassName = "com.r3corda.demos.TraderDemoKt"
|
||||
applicationName = "trader-demo"
|
||||
defaultJvmOpts = ["-javaagent:${configurations.quasar.singleFile}"]
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
// Force windows script classpath to wildcard path to avoid the 'Command Line Is Too Long' issues
|
||||
// with generated scripts. Include Jolokia .war explicitly as this isn't picked up by wildcard
|
||||
tasks.withType(CreateStartScripts) {
|
||||
doLast {
|
||||
windowsScript.text = windowsScript
|
||||
.readLines()
|
||||
.collect { line -> line.replaceAll(~/^set CLASSPATH=.*$/, 'set CLASSPATH=%APP_HOME%/lib/*;%APP_HOME%/lib/jolokia-agent-war-'+project.ext.jolokia_version+'.war') }
|
||||
.join('\r\n')
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: [':node:integrationTest',':client:integrationTest']) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
|
||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
dependsOn = subprojects.test
|
||||
additionalSourceDirs = files(subprojects.sourceSets.main.allSource.srcDirs)
|
||||
@ -181,80 +121,37 @@ tasks.withType(Test) {
|
||||
reports.html.destination = file("${reporting.baseDir}/${name}")
|
||||
}
|
||||
|
||||
quasarScan.dependsOn('classes', 'core:classes', 'contracts:classes', 'node:classes')
|
||||
|
||||
applicationDistribution.into("bin") {
|
||||
from(getAttachmentDemo)
|
||||
from(getTraderDemo)
|
||||
fileMode = 0755
|
||||
}
|
||||
|
||||
task buildCordaJAR(type: FatCapsule, dependsOn: ['quasarScan', 'buildCertSigningRequestUtilityJAR']) {
|
||||
applicationClass 'com.r3corda.node.MainKt'
|
||||
archiveName 'corda.jar'
|
||||
applicationSource = files(project.tasks.findByName('jar'), 'node/build/classes/main/CordaCaplet.class')
|
||||
|
||||
capsuleManifest {
|
||||
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
|
||||
systemProperties['log4j.configuration'] = 'log4j2.xml'
|
||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
|
||||
minJavaVersion = '1.8.0'
|
||||
caplets = ['CordaCaplet']
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "Controller"
|
||||
node {
|
||||
name "Controller"
|
||||
dirName "controller"
|
||||
nearestCity "London"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
artemisPort 10002
|
||||
webPort 10003
|
||||
cordapps = []
|
||||
}
|
||||
}
|
||||
|
||||
task buildCertSigningRequestUtilityJAR(type: FatCapsule, dependsOn: project.jar) {
|
||||
applicationClass 'com.r3corda.node.utilities.certsigning.CertificateSignerKt'
|
||||
archiveName 'certSigningRequestUtility.jar'
|
||||
capsuleManifest {
|
||||
systemProperties['log4j.configuration'] = 'log4j2.xml'
|
||||
minJavaVersion = '1.8.0'
|
||||
node {
|
||||
name "Bank A"
|
||||
dirName "nodea"
|
||||
nearestCity "London"
|
||||
advertisedServices = []
|
||||
artemisPort 10004
|
||||
webPort 10005
|
||||
cordapps = []
|
||||
}
|
||||
}
|
||||
|
||||
task installTemplateNodes(dependsOn: 'buildCordaJAR') << {
|
||||
copy {
|
||||
from buildCordaJAR.outputs.getFiles()
|
||||
from 'config/dev/nameservernode.conf'
|
||||
into "${buildDir}/nodes/nameserver"
|
||||
rename 'nameservernode.conf', 'node.conf'
|
||||
}
|
||||
|
||||
copy {
|
||||
from buildCordaJAR.outputs.getFiles()
|
||||
from 'config/dev/generalnodea.conf'
|
||||
into "${buildDir}/nodes/nodea"
|
||||
rename 'generalnodea.conf', 'node.conf'
|
||||
}
|
||||
|
||||
copy {
|
||||
from buildCordaJAR.outputs.getFiles()
|
||||
from 'config/dev/generalnodeb.conf'
|
||||
into "${buildDir}/nodes/nodeb"
|
||||
rename 'generalnodeb.conf', 'node.conf'
|
||||
}
|
||||
|
||||
delete("${buildDir}/nodes/runnodes")
|
||||
def jarName = buildCordaJAR.outputs.getFiles().getSingleFile().getName()
|
||||
copy {
|
||||
from "buildSrc/scripts/runnodes"
|
||||
filter { String line -> line.replace("JAR_NAME", jarName) }
|
||||
filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf"))
|
||||
into "${buildDir}/nodes"
|
||||
node {
|
||||
name "Bank B"
|
||||
dirName "nodeb"
|
||||
nearestCity "New York"
|
||||
advertisedServices = []
|
||||
artemisPort 10006
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
}
|
||||
}
|
||||
|
||||
// Aliasing the publishToMavenLocal for simplicity.
|
||||
task(install, dependsOn: 'publishToMavenLocal')
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
corda(MavenPublication) {
|
||||
artifactId 'corda'
|
||||
|
||||
artifact buildCordaJAR {
|
||||
classifier ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
0
buildSrc/scripts/runnodes
Normal file → Executable file
0
buildSrc/scripts/runnodes
Normal file → Executable file
@ -1,20 +0,0 @@
|
||||
import org.gradle.api.*
|
||||
import org.gradle.api.tasks.bundling.Jar
|
||||
import org.gradle.api.tasks.javadoc.Javadoc
|
||||
|
||||
/**
|
||||
* A utility plugin that when applied will automatically create source and javadoc publishing tasks
|
||||
*/
|
||||
class DefaultPublishTasks implements Plugin<Project> {
|
||||
void apply(Project project) {
|
||||
project.task("sourceJar", type: Jar, dependsOn: project.classes) {
|
||||
classifier = 'sources'
|
||||
from project.sourceSets.main.allSource
|
||||
}
|
||||
|
||||
project.task("javadocJar", type: Jar, dependsOn: project.javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from project.javadoc.destinationDir
|
||||
}
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import org.gradle.api.*
|
||||
import org.gradle.api.tasks.testing.Test
|
||||
import org.gradle.api.tasks.JavaExec
|
||||
|
||||
/**
|
||||
* QuasarPlugin creates a "quasar" configuration, adds quasar as a dependency and creates a "quasarScan" task that scans
|
||||
* for `@Suspendable`s in the code
|
||||
*/
|
||||
class QuasarPlugin implements Plugin<Project> {
|
||||
void apply(Project project) {
|
||||
|
||||
project.repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
project.configurations.create("quasar")
|
||||
// To add a local .jar dependency:
|
||||
// project.dependencies.add("quasar", project.files("${project.rootProject.projectDir}/lib/quasar.jar"))
|
||||
project.dependencies.add("quasar", "co.paralleluniverse:quasar-core:${project.rootProject.ext.quasar_version}:jdk8@jar")
|
||||
project.dependencies.add("compile", project.configurations.getByName("quasar"))
|
||||
|
||||
project.tasks.withType(Test) {
|
||||
jvmArgs "-javaagent:${project.configurations.quasar.singleFile}"
|
||||
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
|
||||
}
|
||||
project.tasks.withType(JavaExec) {
|
||||
jvmArgs "-javaagent:${project.configurations.quasar.singleFile}"
|
||||
jvmArgs "-Dco.paralleluniverse.fibers.verifyInstrumentation"
|
||||
}
|
||||
|
||||
project.task("quasarScan") {
|
||||
inputs.files(project.sourceSets.main.output)
|
||||
outputs.files(
|
||||
"$project.sourceSets.main.output.resourcesDir/META-INF/suspendables",
|
||||
"$project.sourceSets.main.output.resourcesDir/META-INF/suspendable-supers"
|
||||
)
|
||||
} << {
|
||||
|
||||
// These lines tell gradle to run the Quasar suspendables scanner to look for unannotated super methods
|
||||
// that have @Suspendable sub implementations. These tend to cause NPEs and are not caught by the verifier
|
||||
// NOTE: need to make sure the output isn't on the classpath or every other run it generates empty results, so
|
||||
// we explicitly delete to avoid that happening. We also need to turn off what seems to be a spurious warning in the IDE
|
||||
ant.taskdef(name:'scanSuspendables', classname:'co.paralleluniverse.fibers.instrument.SuspendablesScanner',
|
||||
classpath: "${project.sourceSets.main.output.classesDir}:${project.sourceSets.main.output.resourcesDir}:${project.configurations.runtime.asPath}")
|
||||
project.delete "$project.sourceSets.main.output.resourcesDir/META-INF/suspendables", "$project.sourceSets.main.output.resourcesDir/META-INF/suspendable-supers"
|
||||
ant.scanSuspendables(
|
||||
auto:false,
|
||||
suspendablesFile: "$project.sourceSets.main.output.resourcesDir/META-INF/suspendables",
|
||||
supersFile: "$project.sourceSets.main.output.resourcesDir/META-INF/suspendable-supers") {
|
||||
fileset(dir: project.sourceSets.main.output.classesDir)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
project.jar.dependsOn project.quasarScan
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: QuasarPlugin
|
||||
apply plugin: DefaultPublishTasks
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
@ -80,8 +80,6 @@ dependencies {
|
||||
integrationTestCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
quasarScan.dependsOn('classes', ':core:classes', ':contracts:classes')
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
|
@ -1,62 +0,0 @@
|
||||
package com.r3corda.client
|
||||
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class CordaRPCClientTest {
|
||||
|
||||
private val validUsername = "user1"
|
||||
private val validPassword = "test"
|
||||
private val stopDriver = CountDownLatch(1)
|
||||
private var driverThread: Thread? = null
|
||||
private lateinit var client: CordaRPCClient
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver {
|
||||
val driverInfo = startNode().get()
|
||||
client = CordaRPCClient(toHostAndPort(driverInfo.nodeInfo.address), configureTestSSL())
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with valid username and password`() {
|
||||
client.start(validUsername, validPassword)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with unknown user`() {
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
client.start(random63BitValue().toString(), validPassword)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with incorrect password`() {
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
client.start(validUsername, random63BitValue().toString())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,225 +0,0 @@
|
||||
package com.r3corda.client
|
||||
|
||||
import com.r3corda.client.model.NodeMonitorModel
|
||||
import com.r3corda.client.model.ProgressTrackingEvent
|
||||
import com.r3corda.core.bufferUntilSubscribed
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import com.r3corda.node.services.messaging.StateMachineUpdate
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import com.r3corda.testing.expect
|
||||
import com.r3corda.testing.expectEvents
|
||||
import com.r3corda.testing.sequence
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class NodeMonitorModelTest {
|
||||
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
val stopDriver = CountDownLatch(1)
|
||||
var driverThread: Thread? = null
|
||||
|
||||
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
|
||||
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
|
||||
lateinit var progressTracking: Observable<ProgressTrackingEvent>
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var clientToService: Observer<ClientToServiceCommand>
|
||||
lateinit var newNode: (String) -> NodeInfo
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver {
|
||||
val aliceNodeFuture = startNode("Alice")
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
|
||||
aliceNode = aliceNodeFuture.get().nodeInfo
|
||||
notaryNode = notaryNodeFuture.get().nodeInfo
|
||||
newNode = { nodeName -> startNode(nodeName).get().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
|
||||
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
|
||||
transactions = monitor.transactions.bufferUntilSubscribed()
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
clientToService = monitor.clientToService
|
||||
|
||||
monitor.register(aliceNode, configureTestSSL(), "user1", "test")
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkMapUpdate() {
|
||||
newNode("Bob")
|
||||
newNode("Charlie")
|
||||
networkMapUpdates.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Alice") { output.node.legalIdentity.name }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Bob") { output.node.legalIdentity.name }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Charlie") { output.node.legalIdentity.name }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cashIssueWorksEndToEnd() {
|
||||
clientToService.onNext(ClientToServiceCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
notary = notaryNode.notaryIdentity
|
||||
))
|
||||
|
||||
vaultUpdates.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// SNAPSHOT
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 0) { output.produced.size }
|
||||
},
|
||||
// ISSUE
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 1) { output.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueAndMoveWorks() {
|
||||
|
||||
clientToService.onNext(ClientToServiceCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
notary = notaryNode.notaryIdentity
|
||||
))
|
||||
|
||||
clientToService.onNext(ClientToServiceCommand.PayCash(
|
||||
amount = Amount(100, Issued(PartyAndReference(aliceNode.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||
recipient = aliceNode.legalIdentity
|
||||
))
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
var issueTx: SignedTransaction? = null
|
||||
var moveTx: SignedTransaction? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
},
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == moveSmId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
transactions.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { tx ->
|
||||
require(tx.tx.inputs.isEmpty())
|
||||
require(tx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
|
||||
// Only Alice signed
|
||||
require(signaturePubKeys.size == 1)
|
||||
require(signaturePubKeys.contains(aliceNode.legalIdentity.owningKey))
|
||||
issueTx = tx
|
||||
},
|
||||
// MOVE
|
||||
expect { tx ->
|
||||
require(tx.tx.inputs.size == 1)
|
||||
require(tx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(signaturePubKeys.size == 2)
|
||||
require(signaturePubKeys.contains(aliceNode.legalIdentity.owningKey))
|
||||
require(signaturePubKeys.contains(notaryNode.notaryIdentity.owningKey))
|
||||
moveTx = tx
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
vaultUpdates.expectEvents {
|
||||
sequence(
|
||||
// SNAPSHOT
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 0) { output.produced.size }
|
||||
},
|
||||
// ISSUE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 0) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
stateMachineTransactionMapping.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { mapping ->
|
||||
require(mapping.stateMachineRunId == issueSmId)
|
||||
require(mapping.transactionId == issueTx!!.id)
|
||||
},
|
||||
// MOVE
|
||||
expect { mapping ->
|
||||
require(mapping.stateMachineRunId == moveSmId)
|
||||
require(mapping.transactionId == moveTx!!.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package net.corda.client
|
||||
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.driver.NodeInfoAndConfig
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
|
||||
import net.corda.node.services.messaging.startFlow
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class CordaRPCClientTest {
|
||||
|
||||
private val rpcUser = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
private val stopDriver = CountDownLatch(1)
|
||||
private var driverThread: Thread? = null
|
||||
private lateinit var client: CordaRPCClient
|
||||
private lateinit var driverInfo: NodeInfoAndConfig
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver(isDebug = true) {
|
||||
driverInfo = startNode(rpcUsers = listOf(rpcUser), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))).getOrThrow()
|
||||
client = CordaRPCClient(toHostAndPort(driverInfo.nodeInfo.address), configureTestSSL())
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with valid username and password`() {
|
||||
client.start(rpcUser.username, rpcUser.password)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with unknown user`() {
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
client.start(random63BitValue().toString(), rpcUser.password)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `log in with incorrect password`() {
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
client.start(rpcUser.username, random63BitValue().toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `indefinite block bug`() {
|
||||
println("Starting client")
|
||||
client.start(rpcUser.username, rpcUser.password)
|
||||
println("Creating proxy")
|
||||
val proxy = client.proxy()
|
||||
println("Starting flow")
|
||||
val flowHandle = proxy.startFlow(::CashFlow, CashCommand.IssueCash(20.DOLLARS, OpaqueBytes.of(0), driverInfo.nodeInfo.legalIdentity, driverInfo.nodeInfo.legalIdentity))
|
||||
println("Started flow, waiting on result")
|
||||
flowHandle.progress.subscribe {
|
||||
println("PROGRESS $it")
|
||||
}
|
||||
println("Result: ${flowHandle.returnValue.toBlocking().first()}")
|
||||
}
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
package net.corda.client
|
||||
|
||||
import net.corda.client.model.NodeMonitorModel
|
||||
import net.corda.client.model.ProgressTrackingEvent
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.USD
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import net.corda.node.services.messaging.StateMachineUpdate
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.sequence
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class NodeMonitorModelTest {
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
val stopDriver = CountDownLatch(1)
|
||||
var driverThread: Thread? = null
|
||||
|
||||
lateinit var stateMachineTransactionMapping: Observable<StateMachineTransactionMapping>
|
||||
lateinit var stateMachineUpdates: Observable<StateMachineUpdate>
|
||||
lateinit var progressTracking: Observable<ProgressTrackingEvent>
|
||||
lateinit var transactions: Observable<SignedTransaction>
|
||||
lateinit var vaultUpdates: Observable<Vault.Update>
|
||||
lateinit var networkMapUpdates: Observable<NetworkMapCache.MapChange>
|
||||
lateinit var clientToService: Observer<CashCommand>
|
||||
lateinit var newNode: (String) -> NodeInfo
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
driver {
|
||||
val cashUser = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val aliceNodeFuture = startNode("Alice", rpcUsers = listOf(cashUser))
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
|
||||
aliceNode = aliceNodeFuture.getOrThrow().nodeInfo
|
||||
notaryNode = notaryNodeFuture.getOrThrow().nodeInfo
|
||||
newNode = { nodeName -> startNode(nodeName).getOrThrow().nodeInfo }
|
||||
val monitor = NodeMonitorModel()
|
||||
|
||||
stateMachineTransactionMapping = monitor.stateMachineTransactionMapping.bufferUntilSubscribed()
|
||||
stateMachineUpdates = monitor.stateMachineUpdates.bufferUntilSubscribed()
|
||||
progressTracking = monitor.progressTracking.bufferUntilSubscribed()
|
||||
transactions = monitor.transactions.bufferUntilSubscribed()
|
||||
vaultUpdates = monitor.vaultUpdates.bufferUntilSubscribed()
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
clientToService = monitor.clientToService
|
||||
|
||||
monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network map update`() {
|
||||
newNode("Bob")
|
||||
newNode("Charlie")
|
||||
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
|
||||
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
|
||||
.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Alice") { "Expecting : Alice, Actual : ${output.node.legalIdentity.name}" }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Bob") { "Expecting : Bob, Actual : ${output.node.legalIdentity.name}" }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Charlie") { "Expecting : Charlie, Actual : ${output.node.legalIdentity.name}" }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cash issue works end to end`() {
|
||||
clientToService.onNext(CashCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
notary = notaryNode.notaryIdentity
|
||||
))
|
||||
|
||||
vaultUpdates.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// SNAPSHOT
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 0) { output.produced.size }
|
||||
},
|
||||
// ISSUE
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 1) { output.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cash issue and move`() {
|
||||
clientToService.onNext(CashCommand.IssueCash(
|
||||
amount = Amount(100, USD),
|
||||
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||
recipient = aliceNode.legalIdentity,
|
||||
notary = notaryNode.notaryIdentity
|
||||
))
|
||||
|
||||
clientToService.onNext(CashCommand.PayCash(
|
||||
amount = Amount(100, Issued(PartyAndReference(aliceNode.legalIdentity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||
recipient = aliceNode.legalIdentity
|
||||
))
|
||||
|
||||
var issueSmId: StateMachineRunId? = null
|
||||
var moveSmId: StateMachineRunId? = null
|
||||
var issueTx: SignedTransaction? = null
|
||||
var moveTx: SignedTransaction? = null
|
||||
stateMachineUpdates.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
issueSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == issueSmId)
|
||||
},
|
||||
// MOVE
|
||||
expect { add: StateMachineUpdate.Added ->
|
||||
moveSmId = add.id
|
||||
},
|
||||
expect { remove: StateMachineUpdate.Removed ->
|
||||
require(remove.id == moveSmId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
transactions.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { tx ->
|
||||
require(tx.tx.inputs.isEmpty())
|
||||
require(tx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
|
||||
// Only Alice signed
|
||||
val aliceKey = aliceNode.legalIdentity.owningKey
|
||||
require(signaturePubKeys.size <= aliceKey.keys.size)
|
||||
require(aliceKey.isFulfilledBy(signaturePubKeys))
|
||||
issueTx = tx
|
||||
},
|
||||
// MOVE
|
||||
expect { tx ->
|
||||
require(tx.tx.inputs.size == 1)
|
||||
require(tx.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.sigs.map { it.by }.toSet()
|
||||
// Alice and Notary signed
|
||||
require(aliceNode.legalIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
require(notaryNode.notaryIdentity.owningKey.isFulfilledBy(signaturePubKeys))
|
||||
moveTx = tx
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
vaultUpdates.expectEvents {
|
||||
sequence(
|
||||
// SNAPSHOT
|
||||
expect { output: Vault.Update ->
|
||||
require(output.consumed.size == 0) { output.consumed.size }
|
||||
require(output.produced.size == 0) { output.produced.size }
|
||||
},
|
||||
// ISSUE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 0) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
},
|
||||
// MOVE
|
||||
expect { update ->
|
||||
require(update.consumed.size == 1) { update.consumed.size }
|
||||
require(update.produced.size == 1) { update.produced.size }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
stateMachineTransactionMapping.expectEvents {
|
||||
sequence(
|
||||
// ISSUE
|
||||
expect { mapping ->
|
||||
require(mapping.stateMachineRunId == issueSmId)
|
||||
require(mapping.transactionId == issueTx!!.id)
|
||||
},
|
||||
// MOVE
|
||||
expect { mapping ->
|
||||
require(mapping.stateMachineRunId == moveSmId)
|
||||
require(mapping.transactionId == moveTx!!.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,294 +0,0 @@
|
||||
package com.r3corda.client.impl
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.r3corda.client.CordaRPCClient
|
||||
import com.r3corda.core.ErrorOr
|
||||
import com.r3corda.core.bufferUntilSubscribed
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.serialization.deserialize
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import com.r3corda.core.utilities.debug
|
||||
import com.r3corda.core.utilities.trace
|
||||
import com.r3corda.node.services.messaging.*
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.ClientConsumer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientProducer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.io.Closeable
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.annotation.concurrent.GuardedBy
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
|
||||
/**
|
||||
* Core RPC engine implementation, you should be looking at [CordaRPCClient].
|
||||
*
|
||||
* @suppress
|
||||
*/
|
||||
class CordaRPCClientImpl(private val session: ClientSession,
|
||||
private val sessionLock: ReentrantLock,
|
||||
private val myAddressPrefix: String) {
|
||||
companion object {
|
||||
private val closeableCloseMethod = Closeable::close.javaMethod
|
||||
private val autocloseableCloseMethod = AutoCloseable::close.javaMethod
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a proxy for the given type, which must descend from [RPCOps].
|
||||
*
|
||||
* @see CordaRPCClient.proxy for more information about how to use the proxies.
|
||||
*/
|
||||
fun <T : RPCOps> proxyFor(rpcInterface: Class<T>, timeout: Duration? = null, minVersion: Int = 0): T {
|
||||
sessionLock.withLock {
|
||||
if (producer == null)
|
||||
producer = session.createProducer()
|
||||
}
|
||||
val proxyImpl = RPCProxyHandler(timeout)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val proxy = Proxy.newProxyInstance(rpcInterface.classLoader, arrayOf(rpcInterface, Closeable::class.java), proxyImpl) as T
|
||||
proxyImpl.serverProtocolVersion = proxy.protocolVersion
|
||||
if (minVersion > proxyImpl.serverProtocolVersion)
|
||||
throw RPCException("Requested minimum protocol version $minVersion is higher than the server's supported protocol version (${proxyImpl.serverProtocolVersion})")
|
||||
return proxy
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
//region RPC engine
|
||||
//
|
||||
// You can find docs on all this in the api doc for the proxyFor method, and in the docsite.
|
||||
|
||||
// Utility to quickly suck out the contents of an Artemis message. There's probably a more efficient way to
|
||||
// do this.
|
||||
private fun <T : Any> ClientMessage.deserialize(kryo: Kryo): T = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }.deserialize(kryo)
|
||||
|
||||
@GuardedBy("sessionLock")
|
||||
private val addressToQueueObservables = CacheBuilder.newBuilder().build<String, QueuedObservable>()
|
||||
|
||||
private var producer: ClientProducer? = null
|
||||
|
||||
private inner class ObservableDeserializer(private val qName: String,
|
||||
private val rpcName: String,
|
||||
private val rpcLocation: Throwable) : Serializer<Observable<Any>>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Observable<Any>>): Observable<Any> {
|
||||
val handle = input.readInt(true)
|
||||
return sessionLock.withLock {
|
||||
var ob = addressToQueueObservables.getIfPresent(qName)
|
||||
if (ob == null) {
|
||||
ob = QueuedObservable(qName, rpcName, rpcLocation, this)
|
||||
addressToQueueObservables.put(qName, ob)
|
||||
}
|
||||
val result = ob.getForHandle(handle)
|
||||
rpcLog.trace { "Deserializing and connecting a new observable for $rpcName on $qName: $result" }
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, `object`: Observable<Any>) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The proxy class returned to the client is auto-generated on the fly by the java.lang.reflect Proxy
|
||||
* infrastructure. The JDK Proxy class writes bytecode into memory for a class that implements the requested
|
||||
* interfaces and then routes all method calls to the invoke method below in a conveniently reified form.
|
||||
* We can then easily take the data about the method call and turn it into an RPC. This avoids the need
|
||||
* for the compile-time code generation which is so common in RPC systems.
|
||||
*/
|
||||
@ThreadSafe
|
||||
private inner class RPCProxyHandler(private val timeout: Duration?) : InvocationHandler, Closeable {
|
||||
private val proxyAddress = "$myAddressPrefix.rpc.responses.${random63BitValue()}"
|
||||
private val consumer: ClientConsumer
|
||||
|
||||
var serverProtocolVersion = 0
|
||||
|
||||
init {
|
||||
consumer = sessionLock.withLock{
|
||||
session.createTemporaryQueue(proxyAddress, proxyAddress)
|
||||
session.createConsumer(proxyAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
|
||||
if (isCloseInvocation(method)) {
|
||||
close()
|
||||
return null
|
||||
}
|
||||
if (method.name == "toString" && args == null)
|
||||
return "Client RPC proxy"
|
||||
|
||||
if (consumer.isClosed)
|
||||
throw RPCException("RPC Proxy is closed")
|
||||
|
||||
// All invoked methods on the proxy end up here.
|
||||
val location = Throwable()
|
||||
rpcLog.trace {
|
||||
val argStr = args?.joinToString() ?: ""
|
||||
"-> RPC -> ${method.name}($argStr): ${method.returnType}"
|
||||
}
|
||||
|
||||
checkMethodVersion(method)
|
||||
|
||||
// sendRequest may return a reconfigured Kryo if the method returns observables.
|
||||
val kryo: Kryo = sendRequest(args, location, method) ?: createRPCKryo()
|
||||
val next = receiveResponse(kryo, method, timeout)
|
||||
rpcLog.trace { "<- RPC <- ${method.name} = $next" }
|
||||
return unwrapOrThrow(next)
|
||||
}
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
private fun unwrapOrThrow(next: ErrorOr<*>): Any? {
|
||||
val ex = next.error
|
||||
if (ex != null) {
|
||||
// Replace the stack trace because that's an implementation detail of the server that isn't so
|
||||
// helpful to the user who wants to see where the error was on their side, and serialising stack
|
||||
// frame objects is a bit annoying. We slice it here to avoid the invoke() machinery being exposed.
|
||||
// The resulting exception looks like it was thrown from inside the called method.
|
||||
(ex as java.lang.Throwable).stackTrace = java.lang.Throwable().stackTrace.let { it.sliceArray(1..it.size - 1) }
|
||||
throw ex
|
||||
} else {
|
||||
return next.value
|
||||
}
|
||||
}
|
||||
|
||||
private fun receiveResponse(kryo: Kryo, method: Method, timeout: Duration?): ErrorOr<*> {
|
||||
val artemisMessage: ClientMessage =
|
||||
if (timeout == null)
|
||||
consumer.receive() ?: throw ActiveMQObjectClosedException()
|
||||
else
|
||||
consumer.receive(timeout.toMillis()) ?: throw RPCException.DeadlineExceeded(method.name)
|
||||
artemisMessage.acknowledge()
|
||||
val next = artemisMessage.deserialize<ErrorOr<*>>(kryo)
|
||||
return next
|
||||
}
|
||||
|
||||
private fun sendRequest(args: Array<out Any>?, location: Throwable, method: Method): Kryo? {
|
||||
// We could of course also check the return type of the method to see if it's Observable, but I'd
|
||||
// rather haved the annotation be used consistently.
|
||||
val returnsObservables = method.isAnnotationPresent(RPCReturnsObservables::class.java)
|
||||
|
||||
sessionLock.withLock {
|
||||
val msg = createMessage(method)
|
||||
val kryo = if (returnsObservables) maybePrepareForObservables(location, method, msg) else null
|
||||
val argsArray = args ?: Array<Any?>(0) { null }
|
||||
val serializedBytes = try {
|
||||
argsArray.serialize()
|
||||
} catch (e: KryoException) {
|
||||
throw RPCException("Could not serialize RPC arguments", e)
|
||||
}
|
||||
msg.writeBodyBufferBytes(serializedBytes.bits)
|
||||
producer!!.send(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE, msg)
|
||||
return kryo
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybePrepareForObservables(location: Throwable, method: Method, msg: ClientMessage): Kryo {
|
||||
// Create a temporary queue just for the emissions on any observables that are returned.
|
||||
val qName = "$myAddressPrefix.rpc.observations.${random63BitValue()}"
|
||||
session.createTemporaryQueue(qName, qName)
|
||||
msg.putStringProperty(ClientRPCRequestMessage.OBSERVATIONS_TO, qName)
|
||||
// And make sure that we deserialise observable handles so that they're linked to the right
|
||||
// queue. Also record a bit of metadata for debugging purposes.
|
||||
return createRPCKryo(observableSerializer = ObservableDeserializer(qName, method.name, location))
|
||||
}
|
||||
|
||||
private fun createMessage(method: Method): ClientMessage {
|
||||
return session.createMessage(false).apply {
|
||||
putStringProperty(ClientRPCRequestMessage.METHOD_NAME, method.name)
|
||||
putStringProperty(ClientRPCRequestMessage.REPLY_TO, proxyAddress)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkMethodVersion(method: Method) {
|
||||
val methodVersion = method.getAnnotation(RPCSinceVersion::class.java)?.version ?: 0
|
||||
if (methodVersion > serverProtocolVersion)
|
||||
throw UnsupportedOperationException("Method ${method.name} was added in RPC protocol version $methodVersion but the server is running $serverProtocolVersion")
|
||||
}
|
||||
|
||||
private fun isCloseInvocation(method: Method) = method == closeableCloseMethod || method == autocloseableCloseMethod
|
||||
|
||||
override fun close() {
|
||||
consumer.close()
|
||||
sessionLock.withLock { session.deleteQueue(proxyAddress) }
|
||||
}
|
||||
|
||||
override fun toString() = "Corda RPC Proxy listening on queue $proxyAddress"
|
||||
}
|
||||
|
||||
/**
|
||||
* When subscribed to, starts consuming from the given queue name and demultiplexing the observables being
|
||||
* sent to it. The server queue is moved into in-memory buffers (one per attached server-side observable)
|
||||
* until drained through a subscription. When the subscriptions are all gone, the server-side queue is deleted.
|
||||
*/
|
||||
@ThreadSafe
|
||||
private inner class QueuedObservable(private val qName: String,
|
||||
private val rpcName: String,
|
||||
private val rpcLocation: Throwable,
|
||||
private val observableDeserializer: ObservableDeserializer) {
|
||||
private val root = PublishSubject.create<MarshalledObservation>()
|
||||
private val rootShared = root.doOnUnsubscribe { close() }.share()
|
||||
|
||||
// This could be made more efficient by using a specialised IntMap
|
||||
private val observables = HashMap<Int, Observable<Any>>()
|
||||
|
||||
private var consumer: ClientConsumer? = sessionLock.withLock { session.createConsumer(qName) }.setMessageHandler { deliver(it) }
|
||||
|
||||
@Synchronized
|
||||
fun getForHandle(handle: Int): Observable<Any> {
|
||||
return observables.getOrPut(handle) {
|
||||
rootShared.filter { it.forHandle == handle }.map { it.what }.dematerialize<Any>().bufferUntilSubscribed().share()
|
||||
}
|
||||
}
|
||||
|
||||
private fun deliver(msg: ClientMessage) {
|
||||
msg.acknowledge()
|
||||
val kryo = createRPCKryo(observableSerializer = observableDeserializer)
|
||||
val received: MarshalledObservation = msg.deserialize(kryo)
|
||||
rpcLog.debug { "<- Observable [$rpcName] <- Received $received" }
|
||||
synchronized(this) {
|
||||
// Force creation of the buffer if it doesn't already exist.
|
||||
getForHandle(received.forHandle)
|
||||
root.onNext(received)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun close() {
|
||||
rpcLog.debug("Closing queue observable for call to $rpcName : $qName")
|
||||
consumer?.close()
|
||||
consumer = null
|
||||
sessionLock.withLock { session.deleteQueue(qName) }
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
fun finalize() {
|
||||
val c = synchronized(this) { consumer }
|
||||
if (c != null) {
|
||||
rpcLog.warn("A hot observable returned from an RPC ($rpcName) was never subscribed to or explicitly closed. " +
|
||||
"This wastes server-side resources because it was queueing observations for retrieval. " +
|
||||
"It is being closed now, but please adjust your code to cast the observable to AutoCloseable and then close it explicitly.", rpcLocation)
|
||||
c.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package com.r3corda.client.mock
|
||||
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
||||
* state/ref pairs, but it doesn't necessarily generate "correct" events!
|
||||
*/
|
||||
class EventGenerator(
|
||||
val parties: List<Party>,
|
||||
val notary: Party
|
||||
) {
|
||||
|
||||
private var vault = listOf<StateAndRef<Cash.State>>()
|
||||
|
||||
val issuerGenerator =
|
||||
Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
|
||||
|
||||
val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList()
|
||||
val currencyGenerator = Generator.pickOne(currencies)
|
||||
|
||||
val amountIssuedGenerator =
|
||||
Generator.intRange(1, 10000).combine(issuerGenerator, currencyGenerator) { amount, issuer, currency ->
|
||||
Amount(amount.toLong(), Issued(issuer, currency))
|
||||
}
|
||||
|
||||
val publicKeyGenerator = Generator.oneOf(parties.map { it.owningKey })
|
||||
val partyGenerator = Generator.oneOf(parties)
|
||||
|
||||
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
|
||||
val builder = TransactionBuilder(notary = notary)
|
||||
builder.addOutputState(Cash.State(amount, from))
|
||||
builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
|
||||
builder.toWireTransaction().outRef<Cash.State>(0)
|
||||
}
|
||||
|
||||
val consumedGenerator: Generator<Set<StateRef>> = Generator.frequency(
|
||||
0.7 to Generator.pure(setOf()),
|
||||
0.3 to Generator.impure { vault }.bind { states ->
|
||||
Generator.sampleBernoulli(states, 0.2).map { someStates ->
|
||||
val consumedSet = someStates.map { it.ref }.toSet()
|
||||
vault = vault.filter { it.ref !in consumedSet }
|
||||
consumedSet
|
||||
}
|
||||
}
|
||||
)
|
||||
val producedGenerator: Generator<Set<StateAndRef<ContractState>>> = Generator.frequency(
|
||||
// 0.1 to Generator.pure(setOf())
|
||||
0.9 to Generator.impure { vault }.bind { states ->
|
||||
Generator.replicate(2, cashStateGenerator).map {
|
||||
vault = states + it
|
||||
it.toSet()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
|
||||
|
||||
val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
|
||||
|
||||
val issueCashGenerator =
|
||||
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
|
||||
ClientToServiceCommand.IssueCash(
|
||||
amount,
|
||||
issueRef,
|
||||
to,
|
||||
notary
|
||||
)
|
||||
}
|
||||
|
||||
val moveCashGenerator =
|
||||
amountIssuedGenerator.combine(
|
||||
partyGenerator
|
||||
) { amountIssued, recipient ->
|
||||
ClientToServiceCommand.PayCash(
|
||||
amount = amountIssued,
|
||||
recipient = recipient
|
||||
)
|
||||
}
|
||||
|
||||
val exitCashGenerator =
|
||||
amountIssuedGenerator.map {
|
||||
ClientToServiceCommand.ExitCash(
|
||||
it.withoutIssuer(),
|
||||
it.token.issuer.reference
|
||||
)
|
||||
}
|
||||
|
||||
val clientToServiceCommandGenerator = Generator.frequency(
|
||||
0.4 to issueCashGenerator,
|
||||
0.5 to moveCashGenerator,
|
||||
0.1 to exitCashGenerator
|
||||
)
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
package com.r3corda.client.mock
|
||||
|
||||
import com.r3corda.core.ErrorOr
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This file defines a basic [Generator] library for composing random generators of objects.
|
||||
*
|
||||
* An object of type [Generator]<[A]> captures a generator of [A]s. Generators may be composed in several ways.
|
||||
*
|
||||
* [Generator.choice] picks a generator from the specified list and runs that.
|
||||
* [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking).
|
||||
* [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities.
|
||||
* [Generator.bind] sequences two generators using an arbitrary A->Generator<B> function. Keep the usage of this
|
||||
* function minimal as it may explode the stack, especially when using recursion.
|
||||
*
|
||||
* There are other utilities as well, the type of which are usually descriptive.
|
||||
*
|
||||
* Example:
|
||||
* val birdNameGenerator = Generator.pickOne(listOf("raven", "pigeon"))
|
||||
* val birdHeightGenerator = Generator.doubleRange(from = 10.0, to = 30.0)
|
||||
* val birdGenerator = birdNameGenerator.combine(birdHeightGenerator) { name, height -> Bird(name, height) }
|
||||
* val birdsGenerator = Generator.replicate(2, birdGenerator)
|
||||
* val mammalsGenerator = Generator.sampleBernoulli(listOf(Mammal("fox"), Mammal("elephant")))
|
||||
* val animalsGenerator = Generator.frequency(
|
||||
* 0.2 to birdsGenerator,
|
||||
* 0.8 to mammalsGenerator
|
||||
* )
|
||||
* val animals = animalsGenerator.generate(Random()).getOrThrow()
|
||||
*
|
||||
* The above will generate a random list of animals.
|
||||
*/
|
||||
class Generator<out A>(val generate: (Random) -> ErrorOr<A>) {
|
||||
|
||||
// Functor
|
||||
fun <B> map(function: (A) -> B): Generator<B> =
|
||||
Generator { generate(it).map(function) }
|
||||
|
||||
// Applicative
|
||||
fun <B> product(other: Generator<(A) -> B>) =
|
||||
Generator { generate(it).combine(other.generate(it)) { a, f -> f(a) } }
|
||||
fun <B, R> combine(other1: Generator<B>, function: (A, B) -> R) =
|
||||
product<R>(other1.product(pure({ b -> { a -> function(a, b) } })))
|
||||
fun <B, C, R> combine(other1: Generator<B>, other2: Generator<C>, function: (A, B, C) -> R) =
|
||||
product<R>(other1.product(other2.product(pure({ c -> { b -> { a -> function(a, b, c) } } }))))
|
||||
fun <B, C, D, R> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, function: (A, B, C, D) -> R) =
|
||||
product<R>(other1.product(other2.product(other3.product(pure({ d -> { c -> { b -> { a -> function(a, b, c, d) } } } })))))
|
||||
fun <B, C, D, E, R> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, other4: Generator<E>, function: (A, B, C, D, E) -> R) =
|
||||
product<R>(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } }))))))
|
||||
|
||||
// Monad
|
||||
fun <B> bind(function: (A) -> Generator<B>) =
|
||||
Generator { generate(it).bind { a -> function(a).generate(it) } }
|
||||
|
||||
companion object {
|
||||
fun <A> pure(value: A) = Generator { ErrorOr(value) }
|
||||
fun <A> impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) }
|
||||
fun <A> fail(error: Exception) = Generator<A> { ErrorOr.of(error) }
|
||||
|
||||
// Alternative
|
||||
fun <A> choice(generators: List<Generator<A>>) = intRange(0, generators.size - 1).bind { generators[it] }
|
||||
|
||||
fun <A> success(generate: (Random) -> A) = Generator { ErrorOr(generate(it)) }
|
||||
fun <A> frequency(vararg generators: Pair<Double, Generator<A>>): Generator<A> {
|
||||
val ranges = mutableListOf<Pair<Double, Double>>()
|
||||
var current = 0.0
|
||||
generators.forEach {
|
||||
val next = current + it.first
|
||||
ranges.add(Pair(current, next))
|
||||
current = next
|
||||
}
|
||||
return doubleRange(0.0, current).bind { value ->
|
||||
generators[ranges.binarySearch { range ->
|
||||
if (value < range.first) {
|
||||
1
|
||||
} else if (value < range.second) {
|
||||
0
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}].second
|
||||
}
|
||||
}
|
||||
|
||||
fun <A> sequence(generators: List<Generator<A>>) = Generator<List<A>> {
|
||||
val result = mutableListOf<A>()
|
||||
for (generator in generators) {
|
||||
val element = generator.generate(it)
|
||||
val v = element.value
|
||||
if (v != null) {
|
||||
result.add(v)
|
||||
} else {
|
||||
return@Generator ErrorOr.of(element.error!!)
|
||||
}
|
||||
}
|
||||
ErrorOr(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <A> Generator.Companion.oneOf(list: List<A>) = intRange(0, list.size - 1).map { list[it] }
|
||||
|
||||
fun <A> Generator<A>.generateOrFail(random: Random, numberOfTries: Int = 1): A {
|
||||
var error: Throwable? = null
|
||||
for (i in 0 .. numberOfTries - 1) {
|
||||
val result = generate(random)
|
||||
val v = result.value
|
||||
if (v != null) {
|
||||
return v
|
||||
} else {
|
||||
error = result.error
|
||||
}
|
||||
}
|
||||
if (error == null) {
|
||||
throw IllegalArgumentException("numberOfTries cannot be <= 0")
|
||||
} else {
|
||||
throw Exception("Failed to generate", error)
|
||||
}
|
||||
}
|
||||
|
||||
fun Generator.Companion.int() = Generator.success { it.nextInt() }
|
||||
fun Generator.Companion.intRange(from: Int, to: Int): Generator<Int> = Generator.success {
|
||||
(from + Math.abs(it.nextInt()) % (to - from + 1)).toInt()
|
||||
}
|
||||
fun Generator.Companion.double() = Generator.success { it.nextDouble() }
|
||||
fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double> = Generator.success {
|
||||
from + it.nextDouble() % (to - from)
|
||||
}
|
||||
|
||||
fun <A> Generator.Companion.replicate(number: Int, generator: Generator<A>): Generator<List<A>> {
|
||||
val generators = mutableListOf<Generator<A>>()
|
||||
for (i in 1 .. number) {
|
||||
generators.add(generator)
|
||||
}
|
||||
return sequence(generators)
|
||||
}
|
||||
|
||||
|
||||
fun <A> Generator.Companion.replicatePoisson(meanSize: Double, generator: Generator<A>) = Generator<List<A>> {
|
||||
val chance = (meanSize - 1) / meanSize
|
||||
val result = mutableListOf<A>()
|
||||
var finish = false
|
||||
while (!finish) {
|
||||
val errorOr = Generator.doubleRange(0.0, 1.0).generate(it).bind { value ->
|
||||
if (value < chance) {
|
||||
generator.generate(it).map { result.add(it) }
|
||||
} else {
|
||||
finish = true
|
||||
ErrorOr(Unit)
|
||||
}
|
||||
}
|
||||
val e = errorOr.error
|
||||
if (e != null) {
|
||||
return@Generator ErrorOr.of(e)
|
||||
}
|
||||
}
|
||||
ErrorOr(result)
|
||||
}
|
||||
|
||||
fun <A> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] }
|
||||
|
||||
fun <A> Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) =
|
||||
sampleBernoulli(listOf(collection), maxRatio)
|
||||
fun <A> Generator.Companion.sampleBernoulli(collection: Collection<A>, maxRatio: Double = 1.0): Generator<List<A>> =
|
||||
intRange(0, (maxRatio * collection.size).toInt()).bind { howMany ->
|
||||
replicate(collection.size, Generator.doubleRange(0.0, 1.0)).map { chances ->
|
||||
val result = mutableListOf<A>()
|
||||
collection.forEachIndexed { index, element ->
|
||||
if (chances[index] < howMany.toDouble() / collection.size.toDouble()) {
|
||||
result.add(element)
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.fxutils.foldToObservableList
|
||||
import com.r3corda.client.fxutils.recordInSequence
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.StateRef
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import rx.Observable
|
||||
|
||||
data class Diff<out T : ContractState>(
|
||||
val added: Collection<StateAndRef<T>>,
|
||||
val removed: Collection<StateRef>
|
||||
)
|
||||
|
||||
/**
|
||||
* This model exposes the list of owned contract states.
|
||||
*/
|
||||
class ContractStateModel {
|
||||
private val vaultUpdates: Observable<Vault.Update> by observable(NodeMonitorModel::vaultUpdates)
|
||||
|
||||
val contractStatesDiff: Observable<Diff<ContractState>> = vaultUpdates.map {
|
||||
Diff(it.produced, it.consumed)
|
||||
}
|
||||
val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
|
||||
// We can't filter removed hashes here as we don't have type info
|
||||
Diff(it.added.filterCashStateAndRefs(), it.removed)
|
||||
}
|
||||
val cashStates: ObservableList<StateAndRef<Cash.State>> =
|
||||
cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
|
||||
observableList.removeIf { it.ref in statesDiff.removed }
|
||||
observableList.addAll(statesDiff.added)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> {
|
||||
return this.map { stateAndRef ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (stateAndRef.state.data is Cash.State) {
|
||||
// Kotlin doesn't unify here for some reason
|
||||
stateAndRef as StateAndRef<Cash.State>
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.fxutils.*
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.StateRef
|
||||
import com.r3corda.client.fxutils.recordInSequence
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.services.messaging.StateMachineUpdate
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
|
||||
data class GatheredTransactionData(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
val stateMachines: ObservableList<out StateMachineData>
|
||||
)
|
||||
|
||||
/**
|
||||
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
|
||||
* to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
|
||||
* because of permissioning)
|
||||
*/
|
||||
data class PartiallyResolvedTransaction(
|
||||
val transaction: SignedTransaction,
|
||||
val inputs: List<ObservableValue<InputResolution>>
|
||||
) {
|
||||
val id = transaction.id
|
||||
sealed class InputResolution(val stateRef: StateRef) {
|
||||
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
|
||||
class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSignedTransaction(
|
||||
transaction: SignedTransaction,
|
||||
transactions: ObservableMap<SecureHash, SignedTransaction>
|
||||
) = PartiallyResolvedTransaction(
|
||||
transaction = transaction,
|
||||
inputs = transaction.tx.inputs.map { stateRef ->
|
||||
EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
|
||||
if (it == null) {
|
||||
InputResolution.Unresolved(stateRef)
|
||||
} else {
|
||||
InputResolution.Resolved(it.tx.outRef(stateRef.index))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionCreateStatus(val message: String?) {
|
||||
class Started(message: String?) : TransactionCreateStatus(message)
|
||||
class Failed(message: String?) : TransactionCreateStatus(message)
|
||||
override fun toString(): String = message ?: javaClass.simpleName
|
||||
}
|
||||
|
||||
data class ProtocolStatus(
|
||||
val status: String
|
||||
)
|
||||
|
||||
sealed class StateMachineStatus(val stateMachineName: String) {
|
||||
class Added(stateMachineName: String): StateMachineStatus(stateMachineName)
|
||||
class Removed(stateMachineName: String): StateMachineStatus(stateMachineName)
|
||||
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
|
||||
}
|
||||
|
||||
data class StateMachineData(
|
||||
val id: StateMachineRunId,
|
||||
val protocolStatus: ObservableValue<ProtocolStatus?>,
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus>
|
||||
)
|
||||
|
||||
/**
|
||||
* This model provides an observable list of transactions and what state machines/protocols recorded them
|
||||
*/
|
||||
class GatheredTransactionDataModel {
|
||||
|
||||
private val transactions: Observable<SignedTransaction> by observable(NodeMonitorModel::transactions)
|
||||
private val stateMachineUpdates: Observable<StateMachineUpdate> by observable(NodeMonitorModel::stateMachineUpdates)
|
||||
private val progressTracking: Observable<ProgressTrackingEvent> by observable(NodeMonitorModel::progressTracking)
|
||||
private val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> by observable(NodeMonitorModel::stateMachineTransactionMapping)
|
||||
|
||||
val collectedTransactions = transactions.recordInSequence()
|
||||
val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
|
||||
val stateMachineStatus: ObservableMap<StateMachineRunId, out ObservableValue<StateMachineStatus>> =
|
||||
stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.protocolLogicClassName))
|
||||
map[update.id] = added
|
||||
}
|
||||
is StateMachineUpdate.Removed -> {
|
||||
val added = map[update.id]
|
||||
added ?: throw Exception("State machine removed with unknown id ${update.id}")
|
||||
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
|
||||
}
|
||||
}
|
||||
}
|
||||
val stateMachineDataList: ObservableList<StateMachineData> =
|
||||
LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||
StateMachineData(id, progress.map { it?.let { ProtocolStatus(it.message) } }, status)
|
||||
}.getObservableValues()
|
||||
val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||
val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* We JOIN the transaction list with state machines
|
||||
*/
|
||||
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
||||
partiallyResolvedTransactions.leftOuterJoin(
|
||||
smTxMappingList,
|
||||
PartiallyResolvedTransaction::id,
|
||||
StateMachineTransactionMapping::transactionId
|
||||
) { transaction, mappings ->
|
||||
GatheredTransactionData(
|
||||
transaction,
|
||||
mappings.map { mapping ->
|
||||
stateMachineDataMap.getObservableValue(mapping.stateMachineRunId)
|
||||
}.flatten().filterNotNull()
|
||||
)
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.fxutils.foldToObservableList
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import rx.Observable
|
||||
|
||||
class NetworkIdentityModel {
|
||||
private val networkIdentityObservable: Observable<NetworkMapCache.MapChange> by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
||||
observableList.removeIf {
|
||||
when (update.type) {
|
||||
NetworkMapCache.MapChangeType.Removed -> it == update.node
|
||||
NetworkMapCache.MapChangeType.Modified -> it == update.prevNodeInfo
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
observableList.addAll(update.node)
|
||||
}
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.CordaRPCClient
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.services.config.NodeSSLConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
|
||||
import com.r3corda.node.services.messaging.CordaRPCOps
|
||||
import com.r3corda.node.services.messaging.StateMachineInfo
|
||||
import com.r3corda.node.services.messaging.StateMachineUpdate
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
|
||||
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
|
||||
companion object {
|
||||
fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable<ProgressTrackingEvent>? {
|
||||
return stateMachine.progressTrackerStepAndUpdates?.let { pair ->
|
||||
val (current, future) = pair
|
||||
future.map { ProgressTrackingEvent(stateMachine.id, it) }.startWith(ProgressTrackingEvent(stateMachine.id, current))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This model exposes raw event streams to and from the node.
|
||||
*/
|
||||
class NodeMonitorModel {
|
||||
|
||||
private val stateMachineUpdatesSubject = PublishSubject.create<StateMachineUpdate>()
|
||||
private val vaultUpdatesSubject = PublishSubject.create<Vault.Update>()
|
||||
private val transactionsSubject = PublishSubject.create<SignedTransaction>()
|
||||
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
|
||||
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
|
||||
private val networkMapSubject = PublishSubject.create<NetworkMapCache.MapChange>()
|
||||
|
||||
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
|
||||
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
|
||||
val transactions: Observable<SignedTransaction> = transactionsSubject
|
||||
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
|
||||
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
|
||||
val networkMap: Observable<NetworkMapCache.MapChange> = networkMapSubject
|
||||
|
||||
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
|
||||
val clientToService: PublishSubject<ClientToServiceCommand> = clientToServiceSource
|
||||
|
||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||
|
||||
/**
|
||||
* Register for updates to/from a given vault.
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(vaultMonitorNodeInfo: NodeInfo, sslConfig: NodeSSLConfiguration, username: String, password: String) {
|
||||
val client = CordaRPCClient(toHostAndPort(vaultMonitorNodeInfo.address), sslConfig)
|
||||
client.start(username, password)
|
||||
val proxy = client.proxy()
|
||||
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
|
||||
// Extract the protocol tracking stream
|
||||
// TODO is there a nicer way of doing this? Stream of streams in general results in code like this...
|
||||
val currentProgressTrackerUpdates = stateMachines.mapNotNull { stateMachine ->
|
||||
ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachine)
|
||||
}
|
||||
val futureProgressTrackerUpdates = stateMachineUpdatesSubject.map { stateMachineUpdate ->
|
||||
if (stateMachineUpdate is StateMachineUpdate.Added) {
|
||||
ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachineUpdate.stateMachineInfo) ?: Observable.empty<ProgressTrackingEvent>()
|
||||
} else {
|
||||
Observable.empty<ProgressTrackingEvent>()
|
||||
}
|
||||
}
|
||||
futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.subscribe(progressTrackingSubject)
|
||||
|
||||
// Now the state machines
|
||||
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }
|
||||
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
|
||||
|
||||
// Vault updates
|
||||
val (vault, vaultUpdates) = proxy.vaultAndUpdates()
|
||||
val initialVaultUpdate = Vault.Update(setOf(), vault.toSet())
|
||||
vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject)
|
||||
|
||||
// Transactions
|
||||
val (transactions, newTransactions) = proxy.verifiedTransactions()
|
||||
newTransactions.startWith(transactions).subscribe(transactionsSubject)
|
||||
|
||||
// SM -> TX mapping
|
||||
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping()
|
||||
futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject)
|
||||
|
||||
// Parties on network
|
||||
val (parties, futurePartyUpdate) = proxy.networkMapUpdates()
|
||||
futurePartyUpdate.startWith(parties.map { NetworkMapCache.MapChange(it, null, NetworkMapCache.MapChangeType.Added) }).subscribe(networkMapSubject)
|
||||
|
||||
// Client -> Service
|
||||
clientToServiceSource.subscribe {
|
||||
proxy.executeCommand(it)
|
||||
}
|
||||
|
||||
proxyObservable.set(proxy)
|
||||
}
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
package com.r3corda.client
|
||||
package net.corda.client
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.client.impl.CordaRPCClientImpl
|
||||
import com.r3corda.core.ThreadBox
|
||||
import com.r3corda.node.services.config.NodeSSLConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import com.r3corda.node.services.messaging.CordaRPCOps
|
||||
import com.r3corda.node.services.messaging.RPCException
|
||||
import net.corda.client.impl.CordaRPCClientImpl
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.node.services.config.NodeSSLConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX
|
||||
import net.corda.node.services.messaging.CordaRPCOps
|
||||
import net.corda.node.services.messaging.RPCException
|
||||
import net.corda.node.services.messaging.rpcLog
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQException
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
@ -25,10 +28,6 @@ import kotlin.concurrent.thread
|
||||
*/
|
||||
@ThreadSafe
|
||||
class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfiguration) : Closeable, ArtemisMessagingComponent() {
|
||||
companion object {
|
||||
private val rpcLog = LoggerFactory.getLogger("com.r3corda.rpc")
|
||||
}
|
||||
|
||||
// TODO: Certificate handling for clients needs more work.
|
||||
private inner class State {
|
||||
var running = false
|
||||
@ -36,37 +35,28 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur
|
||||
lateinit var session: ClientSession
|
||||
lateinit var clientImpl: CordaRPCClientImpl
|
||||
}
|
||||
|
||||
private val state = ThreadBox(State())
|
||||
|
||||
/**
|
||||
* An ID that we used to identify this connection on the server side: kind of like a local port number but
|
||||
* it persists for the lifetime of this process and survives short TCP connection interruptions. Is -1
|
||||
* until [start] is called.
|
||||
*/
|
||||
var myID: Int = -1
|
||||
private set
|
||||
|
||||
private val myAddressPrefix: String get() = "${ArtemisMessagingComponent.CLIENTS_PREFIX}$myID"
|
||||
|
||||
/** Opens the connection to the server and registers a JVM shutdown hook to cleanly disconnect. */
|
||||
@Throws(ActiveMQNotConnectedException::class)
|
||||
@Throws(ActiveMQException::class)
|
||||
fun start(username: String, password: String) {
|
||||
state.locked {
|
||||
check(!running)
|
||||
checkStorePasswords() // Check the password.
|
||||
checkStorePasswords()
|
||||
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.OUTBOUND, host.hostText, host.port))
|
||||
serverLocator.threadPoolMaxSize = 1
|
||||
// TODO: Configure session reconnection, confirmation window sizes and other Artemis features.
|
||||
// This will allow reconnection in case of server restart/network outages/IP address changes, etc.
|
||||
// See http://activemq.apache.org/artemis/docs/1.5.0/client-reconnection.html
|
||||
sessionFactory = serverLocator.createSessionFactory()
|
||||
// We use our initial connection ID as the queue namespace.
|
||||
myID = sessionFactory.connection.id as Int and 0x000000FFFFFF
|
||||
session = sessionFactory.createSession(username, password, false, true, true, serverLocator.isPreAcknowledge, serverLocator.ackBatchSize)
|
||||
session.start()
|
||||
clientImpl = CordaRPCClientImpl(session, state.lock, myAddressPrefix)
|
||||
clientImpl = CordaRPCClientImpl(session, state.lock, username)
|
||||
running = true
|
||||
// We will use the ID in strings so strip the sign bit.
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
close()
|
||||
})
|
||||
}
|
||||
@ -91,9 +81,10 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur
|
||||
* calls from many threads at once you should cache one proxy per thread and reuse them. This function itself is
|
||||
* thread safe though so requires no extra synchronisation.
|
||||
*
|
||||
* RPC sends and receives are logged on the com.r3corda.rpc logger.
|
||||
* RPC sends and receives are logged on the net.corda.rpc logger.
|
||||
*
|
||||
* By default there are no timeouts on calls. RPCs can survive temporary losses or changes in connectivity,
|
||||
* By default there are no timeouts on calls. This is deliberate, RPCs without timeouts can survive restarts,
|
||||
* maintenance downtime and moves of the server. RPCs can survive temporary losses or changes in client connectivity,
|
||||
* like switching between wifi networks. You can specify a timeout on the level of a proxy. If a call times
|
||||
* out it will throw [RPCException.Deadline].
|
||||
*
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ListChangeListener
|
@ -1,11 +1,11 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import com.r3corda.client.model.ExchangeRate
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.stream
|
||||
import net.corda.client.model.ExchangeRate
|
||||
import net.corda.core.contracts.Amount
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import java.util.*
|
||||
import java.util.stream.Collectors
|
||||
@ -44,7 +44,7 @@ object AmountBindings {
|
||||
EasyBind.map(
|
||||
Bindings.createLongBinding({
|
||||
amounts.stream().collect(Collectors.summingLong { exchange(it) })
|
||||
} , arrayOf(amounts))
|
||||
}, arrayOf(amounts))
|
||||
) { Amount(it.toLong(), currencyValue) }
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.beans.Observable
|
||||
import javafx.beans.value.ObservableValue
|
||||
@ -22,7 +22,7 @@ import javafx.collections.ObservableListBase
|
||||
*/
|
||||
class ChosenList<E>(
|
||||
private val chosenListObservable: ObservableValue<out ObservableList<out E>>
|
||||
): ObservableListBase<E>() {
|
||||
) : ObservableListBase<E>() {
|
||||
|
||||
private var currentList = chosenListObservable.value
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import co.paralleluniverse.common.util.VisibleForTesting
|
||||
import javafx.collections.ListChangeListener
|
||||
@ -41,6 +41,7 @@ class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : Trans
|
||||
internal val indexMap = HashMap<WrappedObservableList<out A>, Pair<Int, ListChangeListener<A>>>()
|
||||
@VisibleForTesting
|
||||
internal val nestedIndexOffsets = ArrayList<Int>(sourceList.size)
|
||||
|
||||
init {
|
||||
var offset = 0
|
||||
sourceList.forEachIndexed { index, observableList ->
|
||||
@ -78,7 +79,7 @@ class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : Trans
|
||||
permutation[i] = i
|
||||
}
|
||||
// Then the permuted ones.
|
||||
for (i in firstTouched .. startingOffset + change.to - 1) {
|
||||
for (i in firstTouched..startingOffset + change.to - 1) {
|
||||
permutation[startingOffset + i] = change.getPermutation(i)
|
||||
}
|
||||
nextPermutation(firstTouched, startingOffset + change.to, permutation)
|
||||
@ -144,7 +145,7 @@ class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : Trans
|
||||
val newSubNestedIndexOffsets = IntArray(change.to - change.from)
|
||||
val firstTouched = if (change.from == 0) 0 else nestedIndexOffsets[change.from - 1]
|
||||
var currentOffset = firstTouched
|
||||
for (i in 0 .. change.to - change.from - 1) {
|
||||
for (i in 0..change.to - change.from - 1) {
|
||||
currentOffset += source[change.from + i].size
|
||||
newSubNestedIndexOffsets[i] = currentOffset
|
||||
}
|
||||
@ -152,24 +153,24 @@ class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : Trans
|
||||
val concatenatedPermutation = IntArray(newSubNestedIndexOffsets.last())
|
||||
// Set the non-permuted part
|
||||
var offset = 0
|
||||
for (i in 0 .. change.from - 1) {
|
||||
for (i in 0..change.from - 1) {
|
||||
val nestedList = source[i]
|
||||
for (j in offset .. offset + nestedList.size - 1) {
|
||||
for (j in offset..offset + nestedList.size - 1) {
|
||||
concatenatedPermutation[j] = j
|
||||
}
|
||||
offset += nestedList.size
|
||||
}
|
||||
// Now the permuted part
|
||||
for (i in 0 .. newSubNestedIndexOffsets.size - 1) {
|
||||
for (i in 0..newSubNestedIndexOffsets.size - 1) {
|
||||
val startingOffset = startingOffsetOf(change.from + i)
|
||||
val permutedListIndex = change.getPermutation(change.from + i)
|
||||
val permutedOffset = (if (permutedListIndex == 0) 0 else newSubNestedIndexOffsets[permutedListIndex - 1])
|
||||
for (j in 0 .. source[permutedListIndex].size - 1) {
|
||||
for (j in 0..source[permutedListIndex].size - 1) {
|
||||
concatenatedPermutation[startingOffset + j] = permutedOffset + j
|
||||
}
|
||||
}
|
||||
// Record permuted offsets
|
||||
for (i in 0 .. newSubNestedIndexOffsets.size - 1) {
|
||||
for (i in 0..newSubNestedIndexOffsets.size - 1) {
|
||||
nestedIndexOffsets[change.from + i] = newSubNestedIndexOffsets[i]
|
||||
}
|
||||
nextPermutation(firstTouched, newSubNestedIndexOffsets.last(), concatenatedPermutation)
|
||||
@ -229,6 +230,7 @@ class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : Trans
|
||||
|
||||
// Tracks the first position where the *nested* offset is invalid
|
||||
private var firstInvalidatedPosition = sourceList.size
|
||||
|
||||
private fun invalidateOffsets(index: Int) {
|
||||
firstInvalidatedPosition = Math.min(firstInvalidatedPosition, index)
|
||||
}
|
||||
@ -237,7 +239,7 @@ class ConcatenatedList<A>(sourceList: ObservableList<ObservableList<A>>) : Trans
|
||||
if (firstInvalidatedPosition < source.size) {
|
||||
val firstInvalid = firstInvalidatedPosition
|
||||
var offset = if (firstInvalid == 0) 0 else nestedIndexOffsets[firstInvalid - 1]
|
||||
for (i in firstInvalid .. source.size - 1) {
|
||||
for (i in firstInvalid..source.size - 1) {
|
||||
offset += source[i].size
|
||||
if (i < nestedIndexOffsets.size) {
|
||||
nestedIndexOffsets[i] = offset
|
@ -1,12 +1,10 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.beans.InvalidationListener
|
||||
import javafx.beans.value.ChangeListener
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.transformation.TransformationList
|
||||
import org.eclipse.jetty.server.Authentication
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -30,7 +28,9 @@ class FlattenedList<A>(val sourceList: ObservableList<out ObservableValue<out A>
|
||||
class WrappedObservableValue<A>(
|
||||
val observableValue: ObservableValue<A>
|
||||
)
|
||||
|
||||
val indexMap = HashMap<WrappedObservableValue<out A>, Pair<Int, ChangeListener<A>>>()
|
||||
|
||||
init {
|
||||
sourceList.forEachIndexed { index, observableValue ->
|
||||
val wrappedObservableValue = WrappedObservableValue(observableValue)
|
@ -1,8 +1,9 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.*
|
||||
import javafx.collections.MapChangeListener
|
||||
import javafx.collections.ObservableMap
|
||||
|
||||
/**
|
||||
* [LeftOuterJoinedMap] implements a special case of a left outer join where we're matching on primary keys of both
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.MapChangeListener
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.application.Platform
|
||||
import javafx.beans.property.SimpleObjectProperty
|
@ -1,6 +1,7 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.binding.BooleanBinding
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
@ -10,9 +11,7 @@ import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import javafx.collections.transformation.FilteredList
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.function.Predicate
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should
|
||||
@ -49,18 +48,22 @@ fun <A, B> ObservableList<out A>.map(cached: Boolean = true, function: (A) -> B)
|
||||
* val aliceHeightPlus2 = ::sumHeight.lift(aliceHeight, 2L.lift())
|
||||
*/
|
||||
fun <A> A.lift(): ObservableValue<A> = ReadOnlyObjectWrapper(this)
|
||||
|
||||
fun <A, R> ((A) -> R).lift(
|
||||
arg0: ObservableValue<A>
|
||||
): ObservableValue<R> = EasyBind.map(arg0, this)
|
||||
|
||||
fun <A, B, R> ((A, B) -> R).lift(
|
||||
arg0: ObservableValue<A>,
|
||||
arg1: ObservableValue<B>
|
||||
): ObservableValue<R> = EasyBind.combine(arg0, arg1, this)
|
||||
|
||||
fun <A, B, C, R> ((A, B, C) -> R).lift(
|
||||
arg0: ObservableValue<A>,
|
||||
arg1: ObservableValue<B>,
|
||||
arg2: ObservableValue<C>
|
||||
): ObservableValue<R> = EasyBind.combine(arg0, arg1, arg2, this)
|
||||
|
||||
fun <A, B, C, D, R> ((A, B, C, D) -> R).lift(
|
||||
arg0: ObservableValue<A>,
|
||||
arg1: ObservableValue<B>,
|
||||
@ -75,6 +78,7 @@ fun <A, B, C, D, R> ((A, B, C, D) -> R).lift(
|
||||
*/
|
||||
fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<B>): ObservableValue<B> =
|
||||
EasyBind.monadic(this).flatMap(function)
|
||||
|
||||
/**
|
||||
* A variant of [bind] that has out variance on the output type. This is sometimes useful when kotlin is too eager to
|
||||
* propagate variance constraints and type inference fails.
|
||||
@ -265,9 +269,11 @@ fun <A : Any, B : Any, K : Any> ObservableList<A>.leftOuterJoin(
|
||||
fun <A> ObservableList<A>.getValueAt(index: Int): ObservableValue<A?> {
|
||||
return Bindings.valueAt(this, index)
|
||||
}
|
||||
|
||||
fun <A> ObservableList<A>.first(): ObservableValue<A?> {
|
||||
return getValueAt(0)
|
||||
}
|
||||
|
||||
fun <A> ObservableList<A>.last(): ObservableValue<A?> {
|
||||
return Bindings.createObjectBinding({
|
||||
if (size > 0) {
|
||||
@ -277,3 +283,27 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
|
||||
}
|
||||
}, arrayOf(this))
|
||||
}
|
||||
|
||||
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> {
|
||||
return AggregatedList(this, { it }, { key, _list -> key })
|
||||
}
|
||||
|
||||
fun ObservableValue<*>.isNotNull(): BooleanBinding {
|
||||
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return first element of the observable list as observable value.
|
||||
* Return provided default value if the list is empty.
|
||||
*/
|
||||
fun <A> ObservableList<A>.firstOrDefault(default: ObservableValue<A?>, predicate: (A) -> Boolean): ObservableValue<A?> {
|
||||
return Bindings.createObjectBinding({ this.firstOrNull(predicate) ?: default.value }, arrayOf(this, default))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return first element of the observable list as observable value.
|
||||
* Return ObservableValue(null) if the list is empty.
|
||||
*/
|
||||
fun <A> ObservableList<A>.firstOrNullObservable(predicate: (A) -> Boolean): ObservableValue<A?> {
|
||||
return Bindings.createObjectBinding({ this.firstOrNull(predicate) }, arrayOf(this))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import com.sun.javafx.collections.MapListenerHelper
|
||||
import javafx.beans.InvalidationListener
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
@ -25,7 +25,7 @@ class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(
|
||||
val permutation = IntArray(to, { c.getPermutation(it) })
|
||||
val permutedSubList = ArrayList<A?>(to - from)
|
||||
permutedSubList.addAll(Collections.nCopies(to - from, null))
|
||||
for (i in 0 .. (to - from - 1)) {
|
||||
for (i in 0..(to - from - 1)) {
|
||||
permutedSubList[permutation[from + i]] = replayedList[from + i]
|
||||
}
|
||||
permutedSubList.forEachIndexed { i, element ->
|
||||
@ -33,14 +33,14 @@ class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(
|
||||
}
|
||||
nextPermutation(from, to, permutation)
|
||||
} else if (c.wasUpdated()) {
|
||||
for (i in c.from .. c.to - 1) {
|
||||
for (i in c.from..c.to - 1) {
|
||||
replayedList[i] = c.list[i]
|
||||
nextUpdate(i)
|
||||
}
|
||||
} else {
|
||||
if (c.wasRemoved()) {
|
||||
val removePosition = c.from
|
||||
for (i in 0 .. c.removedSize - 1) {
|
||||
for (i in 0..c.removedSize - 1) {
|
||||
replayedList.removeAt(removePosition)
|
||||
}
|
||||
nextRemove(c.from, c.removed)
|
||||
@ -48,7 +48,7 @@ class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(
|
||||
if (c.wasAdded()) {
|
||||
val addStart = c.from
|
||||
val addEnd = c.to
|
||||
for (i in addStart .. addEnd - 1) {
|
||||
for (i in addStart..addEnd - 1) {
|
||||
replayedList.add(i, c.list[i])
|
||||
}
|
||||
nextAdd(addStart, addEnd)
|
@ -0,0 +1,336 @@
|
||||
package net.corda.client.impl
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.esotericsoftware.kryo.KryoException
|
||||
import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import net.corda.client.CordaRPCClient
|
||||
import net.corda.core.ErrorOr
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.messaging.*
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
|
||||
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.ClientConsumer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientProducer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.io.Closeable
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import javax.annotation.concurrent.GuardedBy
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
|
||||
/**
|
||||
* Core RPC engine implementation, to learn how to use RPC you should be looking at [CordaRPCClient].
|
||||
*
|
||||
* # Design notes
|
||||
*
|
||||
* The way RPCs are handled is fairly standard except for the handling of observables. When an RPC might return
|
||||
* an [Observable] it is specially tagged. This causes the client to create a new transient queue for the
|
||||
* receiving of observables and their observations with a random ID in the name. This ID is sent to the server in
|
||||
* a message header. All observations are sent via this single queue.
|
||||
*
|
||||
* The reason for doing it this way and not the more obvious approach of one-queue-per-observable is that we want
|
||||
* the queues to be *transient*, meaning their lifetime in the broker is tied to the session that created them.
|
||||
* A server side observable and its associated queue is not a cost-free thing, let alone the memory and resources
|
||||
* needed to actually generate the observations themselves, therefore we want to ensure these cannot leak. A
|
||||
* transient queue will be deleted automatically if the client session terminates, which by default happens on
|
||||
* disconnect but can also be configured to happen after a short delay (this allows clients to e.g. switch IP
|
||||
* address). On the server the deletion of the observations queue triggers unsubscription from the associated
|
||||
* observables, which in turn may then be garbage collected.
|
||||
*
|
||||
* Creating a transient queue requires a roundtrip to the broker and thus doing an RPC that could return
|
||||
* observables takes two server roundtrips instead of one. That's why we require RPCs to be marked with
|
||||
* [RPCReturnsObservables] as needing this special treatment instead of always doing it.
|
||||
*
|
||||
* If the Artemis/JMS APIs allowed us to create transient queues assigned to someone else then we could
|
||||
* potentially use a different design in which the node creates new transient queues (one per observable) on the
|
||||
* fly. The client would then have to watch out for this and start consuming those queues as they were created.
|
||||
*
|
||||
* We use one queue per RPC because we don't know ahead of time how many observables the server might return and
|
||||
* often the server doesn't know either, which pushes towards a single queue design, but at the same time the
|
||||
* processing of observations returned by an RPC might be striped across multiple threads and we'd like
|
||||
* backpressure management to not be scoped per client process but with more granularity. So we end up with
|
||||
* a compromise where the unit of backpressure management is the response to a single RPC.
|
||||
*
|
||||
* TODO: Backpressure isn't propagated all the way through the MQ broker at the moment.
|
||||
*/
|
||||
class CordaRPCClientImpl(private val session: ClientSession,
|
||||
private val sessionLock: ReentrantLock,
|
||||
private val username: String) {
|
||||
companion object {
|
||||
private val closeableCloseMethod = Closeable::close.javaMethod
|
||||
private val autocloseableCloseMethod = AutoCloseable::close.javaMethod
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a proxy for the given type, which must descend from [RPCOps].
|
||||
*
|
||||
* @see CordaRPCClient.proxy for more information about how to use the proxies.
|
||||
*/
|
||||
fun <T : RPCOps> proxyFor(rpcInterface: Class<T>, timeout: Duration? = null, minVersion: Int = 0): T {
|
||||
sessionLock.withLock {
|
||||
if (producer == null)
|
||||
producer = session.createProducer()
|
||||
}
|
||||
val proxyImpl = RPCProxyHandler(timeout)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val proxy = Proxy.newProxyInstance(rpcInterface.classLoader, arrayOf(rpcInterface, Closeable::class.java), proxyImpl) as T
|
||||
proxyImpl.serverProtocolVersion = proxy.protocolVersion
|
||||
if (minVersion > proxyImpl.serverProtocolVersion)
|
||||
throw RPCException("Requested minimum protocol version $minVersion is higher than the server's supported protocol version (${proxyImpl.serverProtocolVersion})")
|
||||
return proxy
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
//region RPC engine
|
||||
//
|
||||
// You can find docs on all this in the api doc for the proxyFor method, and in the docsite.
|
||||
|
||||
// Utility to quickly suck out the contents of an Artemis message. There's probably a more efficient way to
|
||||
// do this.
|
||||
private fun <T : Any> ClientMessage.deserialize(kryo: Kryo): T = ByteArray(bodySize).apply { bodyBuffer.readBytes(this) }.deserialize(kryo)
|
||||
|
||||
@GuardedBy("sessionLock")
|
||||
private val addressToQueueObservables = CacheBuilder.newBuilder().build<String, QueuedObservable>()
|
||||
|
||||
private var producer: ClientProducer? = null
|
||||
|
||||
private inner class ObservableDeserializer(private val qName: String,
|
||||
private val rpcName: String,
|
||||
private val rpcLocation: Throwable) : Serializer<Observable<Any>>() {
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<Observable<Any>>): Observable<Any> {
|
||||
val handle = input.readInt(true)
|
||||
val ob = sessionLock.withLock {
|
||||
addressToQueueObservables.getIfPresent(qName) ?: QueuedObservable(qName, rpcName, rpcLocation, this).apply {
|
||||
addressToQueueObservables.put(qName, this)
|
||||
}
|
||||
}
|
||||
val result = ob.getForHandle(handle)
|
||||
rpcLog.debug { "Deserializing and connecting a new observable for $rpcName on $qName: $result" }
|
||||
return result
|
||||
}
|
||||
|
||||
override fun write(kryo: Kryo, output: Output, `object`: Observable<Any>) {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The proxy class returned to the client is auto-generated on the fly by the java.lang.reflect Proxy
|
||||
* infrastructure. The JDK Proxy class writes bytecode into memory for a class that implements the requested
|
||||
* interfaces and then routes all method calls to the invoke method below in a conveniently reified form.
|
||||
* We can then easily take the data about the method call and turn it into an RPC. This avoids the need
|
||||
* for the compile-time code generation which is so common in RPC systems.
|
||||
*/
|
||||
@ThreadSafe
|
||||
private inner class RPCProxyHandler(private val timeout: Duration?) : InvocationHandler, Closeable {
|
||||
private val proxyId = random63BitValue()
|
||||
private val consumer: ClientConsumer
|
||||
|
||||
var serverProtocolVersion = 0
|
||||
|
||||
init {
|
||||
val proxyAddress = constructAddress(proxyId)
|
||||
consumer = sessionLock.withLock {
|
||||
session.createTemporaryQueue(proxyAddress, proxyAddress)
|
||||
session.createConsumer(proxyAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun constructAddress(addressId: Long) = "${ArtemisMessagingComponent.CLIENTS_PREFIX}$username.rpc.$addressId"
|
||||
|
||||
@Synchronized
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
|
||||
if (isCloseInvocation(method)) {
|
||||
close()
|
||||
return null
|
||||
}
|
||||
if (method.name == "toString" && args == null)
|
||||
return "Client RPC proxy"
|
||||
|
||||
if (consumer.isClosed)
|
||||
throw RPCException("RPC Proxy is closed")
|
||||
|
||||
// All invoked methods on the proxy end up here.
|
||||
val location = Throwable()
|
||||
rpcLog.debug {
|
||||
val argStr = args?.joinToString() ?: ""
|
||||
"-> RPC -> ${method.name}($argStr): ${method.returnType}"
|
||||
}
|
||||
|
||||
checkMethodVersion(method)
|
||||
|
||||
// sendRequest may return a reconfigured Kryo if the method returns observables.
|
||||
val kryo: Kryo = sendRequest(args, location, method) ?: createRPCKryo()
|
||||
val next: ErrorOr<*> = receiveResponse(kryo, method, timeout)
|
||||
rpcLog.debug { "<- RPC <- ${method.name} = $next" }
|
||||
return unwrapOrThrow(next)
|
||||
}
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
private fun unwrapOrThrow(next: ErrorOr<*>): Any? {
|
||||
val ex = next.error
|
||||
if (ex != null) {
|
||||
// Replace the stack trace because that's an implementation detail of the server that isn't so
|
||||
// helpful to the user who wants to see where the error was on their side, and serialising stack
|
||||
// frame objects is a bit annoying. We slice it here to avoid the invoke() machinery being exposed.
|
||||
// The resulting exception looks like it was thrown from inside the called method.
|
||||
(ex as java.lang.Throwable).stackTrace = java.lang.Throwable().stackTrace.let { it.sliceArray(1..it.size - 1) }
|
||||
throw ex
|
||||
} else {
|
||||
return next.value
|
||||
}
|
||||
}
|
||||
|
||||
private fun receiveResponse(kryo: Kryo, method: Method, timeout: Duration?): ErrorOr<*> {
|
||||
val artemisMessage: ClientMessage =
|
||||
if (timeout == null)
|
||||
consumer.receive() ?: throw ActiveMQObjectClosedException()
|
||||
else
|
||||
consumer.receive(timeout.toMillis()) ?: throw RPCException.DeadlineExceeded(method.name)
|
||||
artemisMessage.acknowledge()
|
||||
val next = artemisMessage.deserialize<ErrorOr<*>>(kryo)
|
||||
return next
|
||||
}
|
||||
|
||||
private fun sendRequest(args: Array<out Any>?, location: Throwable, method: Method): Kryo? {
|
||||
// We could of course also check the return type of the method to see if it's Observable, but I'd
|
||||
// rather haved the annotation be used consistently.
|
||||
val returnsObservables = method.isAnnotationPresent(RPCReturnsObservables::class.java)
|
||||
|
||||
sessionLock.withLock {
|
||||
val msg: ClientMessage = createMessage(method)
|
||||
val kryo = if (returnsObservables) maybePrepareForObservables(location, method, msg) else null
|
||||
val serializedArgs = try {
|
||||
(args ?: emptyArray<Any?>()).serialize(createRPCKryo())
|
||||
} catch (e: KryoException) {
|
||||
throw RPCException("Could not serialize RPC arguments", e)
|
||||
}
|
||||
msg.writeBodyBufferBytes(serializedArgs.bytes)
|
||||
producer!!.send(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE, msg)
|
||||
return kryo
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybePrepareForObservables(location: Throwable, method: Method, msg: ClientMessage): Kryo {
|
||||
// Create a temporary queue just for the emissions on any observables that are returned.
|
||||
val observationsId = random63BitValue()
|
||||
val observationsQueueName = constructAddress(observationsId)
|
||||
session.createTemporaryQueue(observationsQueueName, observationsQueueName)
|
||||
msg.putLongProperty(ClientRPCRequestMessage.OBSERVATIONS_TO, observationsId)
|
||||
// And make sure that we deserialise observable handles so that they're linked to the right
|
||||
// queue. Also record a bit of metadata for debugging purposes.
|
||||
return createRPCKryo(observableSerializer = ObservableDeserializer(observationsQueueName, method.name, location))
|
||||
}
|
||||
|
||||
private fun createMessage(method: Method): ClientMessage {
|
||||
return session.createMessage(false).apply {
|
||||
putStringProperty(ClientRPCRequestMessage.METHOD_NAME, method.name)
|
||||
putLongProperty(ClientRPCRequestMessage.REPLY_TO, proxyId)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkMethodVersion(method: Method) {
|
||||
val methodVersion = method.getAnnotation(RPCSinceVersion::class.java)?.version ?: 0
|
||||
if (methodVersion > serverProtocolVersion)
|
||||
throw UnsupportedOperationException("Method ${method.name} was added in RPC protocol version $methodVersion but the server is running $serverProtocolVersion")
|
||||
}
|
||||
|
||||
private fun isCloseInvocation(method: Method) = method == closeableCloseMethod || method == autocloseableCloseMethod
|
||||
|
||||
override fun close() {
|
||||
consumer.close()
|
||||
sessionLock.withLock { session.deleteQueue(constructAddress(proxyId)) }
|
||||
}
|
||||
|
||||
override fun toString() = "Corda RPC Proxy listening on queue ${constructAddress(proxyId)}"
|
||||
}
|
||||
|
||||
/**
|
||||
* When subscribed to, starts consuming from the given queue name and demultiplexing the observables being
|
||||
* sent to it. The server queue is moved into in-memory buffers (one per attached server-side observable)
|
||||
* until drained through a subscription. When the subscriptions are all gone, the server-side queue is deleted.
|
||||
*/
|
||||
@ThreadSafe
|
||||
private inner class QueuedObservable(private val qName: String,
|
||||
private val rpcName: String,
|
||||
private val rpcLocation: Throwable,
|
||||
private val observableDeserializer: ObservableDeserializer) {
|
||||
private val root = PublishSubject.create<MarshalledObservation>()
|
||||
private val rootShared = root.doOnUnsubscribe { close() }.share()
|
||||
|
||||
// This could be made more efficient by using a specialised IntMap
|
||||
private val observables = HashMap<Int, Observable<Any>>()
|
||||
|
||||
private var consumer: ClientConsumer? = sessionLock.withLock { session.createConsumer(qName) }.setMessageHandler { deliver(it) }
|
||||
|
||||
@Synchronized
|
||||
fun getForHandle(handle: Int): Observable<Any> {
|
||||
return observables.getOrPut(handle) {
|
||||
/**
|
||||
* Note that the order of bufferUntilSubscribed() -> dematerialize() is very important here.
|
||||
*
|
||||
* In particular doing it the other way around may result in the following edge case:
|
||||
* The RPC returns two (or more) Observables. The first Observable unsubscribes *during serialisation*,
|
||||
* before the second one is hit, causing the [rootShared] to unsubscribe and consequently closing
|
||||
* the underlying artemis queue, even though the second Observable was not even registered.
|
||||
*
|
||||
* The buffer -> dematerialize order ensures that the Observable may not unsubscribe until the caller
|
||||
* subscribes, which must be after full deserialisation and registering of all top level Observables.
|
||||
*/
|
||||
rootShared.filter { it.forHandle == handle }.map { it.what }.bufferUntilSubscribed().dematerialize<Any>().share()
|
||||
}
|
||||
}
|
||||
|
||||
private fun deliver(msg: ClientMessage) {
|
||||
msg.acknowledge()
|
||||
val kryo = createRPCKryo(observableSerializer = observableDeserializer)
|
||||
val received: MarshalledObservation = msg.deserialize(kryo)
|
||||
rpcLog.debug { "<- Observable [$rpcName] <- Received $received" }
|
||||
synchronized(this) {
|
||||
// Force creation of the buffer if it doesn't already exist.
|
||||
getForHandle(received.forHandle)
|
||||
root.onNext(received)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun close() {
|
||||
rpcLog.debug("Closing queue observable for call to $rpcName : $qName")
|
||||
consumer?.close()
|
||||
consumer = null
|
||||
sessionLock.withLock { session.deleteQueue(qName) }
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
fun finalize() {
|
||||
val c = synchronized(this) { consumer }
|
||||
if (c != null) {
|
||||
rpcLog.warn("A hot observable returned from an RPC ($rpcName) was never subscribed to or explicitly closed. " +
|
||||
"This wastes server-side resources because it was queueing observations for retrieval. " +
|
||||
"It is being closed now, but please adjust your code to cast the observable to AutoCloseable and then close it explicitly.", rpcLocation)
|
||||
c.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
100
client/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
Normal file
100
client/src/main/kotlin/net/corda/client/mock/EventGenerator.kt
Normal file
@ -0,0 +1,100 @@
|
||||
package net.corda.client.mock
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.flows.CashCommand
|
||||
|
||||
/**
|
||||
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
|
||||
* state/ref pairs, but it doesn't necessarily generate "correct" events!
|
||||
*/
|
||||
class EventGenerator(
|
||||
val parties: List<Party>,
|
||||
val notary: Party
|
||||
) {
|
||||
|
||||
private var vault = listOf<StateAndRef<Cash.State>>()
|
||||
|
||||
val issuerGenerator =
|
||||
Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) }
|
||||
|
||||
val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList()
|
||||
val currencyGenerator = Generator.pickOne(currencies)
|
||||
|
||||
val issuedGenerator = issuerGenerator.combine(currencyGenerator) { issuer, currency -> Issued(issuer, currency) }
|
||||
val amountIssuedGenerator = generateAmount(1, 10000, issuedGenerator)
|
||||
|
||||
val publicKeyGenerator = Generator.pickOne(parties.map { it.owningKey })
|
||||
val partyGenerator = Generator.pickOne(parties)
|
||||
|
||||
val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from ->
|
||||
val builder = TransactionBuilder(notary = notary)
|
||||
builder.addOutputState(Cash.State(amount, from))
|
||||
builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey))
|
||||
builder.toWireTransaction().outRef<Cash.State>(0)
|
||||
}
|
||||
|
||||
val consumedGenerator: Generator<Set<StateRef>> = Generator.frequency(
|
||||
0.7 to Generator.pure(setOf()),
|
||||
0.3 to Generator.impure { vault }.bind { states ->
|
||||
Generator.sampleBernoulli(states, 0.2).map { someStates ->
|
||||
val consumedSet = someStates.map { it.ref }.toSet()
|
||||
vault = vault.filter { it.ref !in consumedSet }
|
||||
consumedSet
|
||||
}
|
||||
}
|
||||
)
|
||||
val producedGenerator: Generator<Set<StateAndRef<ContractState>>> = Generator.frequency(
|
||||
// 0.1 to Generator.pure(setOf())
|
||||
0.9 to Generator.impure { vault }.bind { states ->
|
||||
Generator.replicate(2, cashStateGenerator).map {
|
||||
vault = states + it
|
||||
it.toSet()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
|
||||
|
||||
val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) }
|
||||
|
||||
val issueCashGenerator =
|
||||
amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef ->
|
||||
CashCommand.IssueCash(
|
||||
amount,
|
||||
issueRef,
|
||||
to,
|
||||
notary
|
||||
)
|
||||
}
|
||||
|
||||
val moveCashGenerator =
|
||||
amountIssuedGenerator.combine(
|
||||
partyGenerator
|
||||
) { amountIssued, recipient ->
|
||||
CashCommand.PayCash(
|
||||
amount = amountIssued,
|
||||
recipient = recipient
|
||||
)
|
||||
}
|
||||
|
||||
val exitCashGenerator =
|
||||
amountIssuedGenerator.map {
|
||||
CashCommand.ExitCash(
|
||||
it.withoutIssuer(),
|
||||
it.token.issuer.reference
|
||||
)
|
||||
}
|
||||
|
||||
val clientCommandGenerator = Generator.frequency(
|
||||
1.0 to moveCashGenerator
|
||||
)
|
||||
|
||||
val bankOfCordaCommandGenerator = Generator.frequency(
|
||||
0.6 to issueCashGenerator,
|
||||
0.4 to exitCashGenerator
|
||||
)
|
||||
}
|
211
client/src/main/kotlin/net/corda/client/mock/Generator.kt
Normal file
211
client/src/main/kotlin/net/corda/client/mock/Generator.kt
Normal file
@ -0,0 +1,211 @@
|
||||
package net.corda.client.mock
|
||||
|
||||
import net.corda.client.mock.Generator.Companion.choice
|
||||
import net.corda.core.ErrorOr
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This file defines a basic [Generator] library for composing random generators of objects.
|
||||
*
|
||||
* An object of type [Generator]<[A]> captures a generator of [A]s. Generators may be composed in several ways.
|
||||
*
|
||||
* [Generator.choice] picks a generator from the specified list and runs that.
|
||||
* [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking).
|
||||
* [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities.
|
||||
* [Generator.bind] sequences two generators using an arbitrary A->Generator<B> function. Keep the usage of this
|
||||
* function minimal as it may explode the stack, especially when using recursion.
|
||||
*
|
||||
* There are other utilities as well, the type of which are usually descriptive.
|
||||
*
|
||||
* Example:
|
||||
* val birdNameGenerator = Generator.pickOne(listOf("raven", "pigeon"))
|
||||
* val birdHeightGenerator = Generator.doubleRange(from = 10.0, to = 30.0)
|
||||
* val birdGenerator = birdNameGenerator.combine(birdHeightGenerator) { name, height -> Bird(name, height) }
|
||||
* val birdsGenerator = Generator.replicate(2, birdGenerator)
|
||||
* val mammalsGenerator = Generator.sampleBernoulli(listOf(Mammal("fox"), Mammal("elephant")))
|
||||
* val animalsGenerator = Generator.frequency(
|
||||
* 0.2 to birdsGenerator,
|
||||
* 0.8 to mammalsGenerator
|
||||
* )
|
||||
* val animals = animalsGenerator.generate(SplittableRandom()).getOrThrow()
|
||||
*
|
||||
* The above will generate a random list of animals.
|
||||
*/
|
||||
class Generator<out A : Any>(val generate: (SplittableRandom) -> ErrorOr<A>) {
|
||||
|
||||
// Functor
|
||||
fun <B : Any> map(function: (A) -> B): Generator<B> =
|
||||
Generator { generate(it).map(function) }
|
||||
|
||||
// Applicative
|
||||
fun <B : Any> product(other: Generator<(A) -> B>) =
|
||||
Generator { generate(it).combine(other.generate(it)) { a, f -> f(a) } }
|
||||
|
||||
fun <B : Any, R : Any> combine(other1: Generator<B>, function: (A, B) -> R) =
|
||||
product<R>(other1.product(pure({ b -> { a -> function(a, b) } })))
|
||||
|
||||
fun <B : Any, C : Any, R : Any> combine(other1: Generator<B>, other2: Generator<C>, function: (A, B, C) -> R) =
|
||||
product<R>(other1.product(other2.product(pure({ c -> { b -> { a -> function(a, b, c) } } }))))
|
||||
|
||||
fun <B : Any, C : Any, D : Any, R : Any> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, function: (A, B, C, D) -> R) =
|
||||
product<R>(other1.product(other2.product(other3.product(pure({ d -> { c -> { b -> { a -> function(a, b, c, d) } } } })))))
|
||||
|
||||
fun <B : Any, C : Any, D : Any, E : Any, R : Any> combine(other1: Generator<B>, other2: Generator<C>, other3: Generator<D>, other4: Generator<E>, function: (A, B, C, D, E) -> R) =
|
||||
product<R>(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } }))))))
|
||||
|
||||
// Monad
|
||||
fun <B : Any> bind(function: (A) -> Generator<B>) =
|
||||
Generator { generate(it).bind { a -> function(a).generate(it) } }
|
||||
|
||||
companion object {
|
||||
fun <A : Any> pure(value: A) = Generator { ErrorOr(value) }
|
||||
fun <A : Any> impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) }
|
||||
fun <A : Any> fail(error: Exception) = Generator<A> { ErrorOr.of(error) }
|
||||
|
||||
// Alternative
|
||||
fun <A : Any> choice(generators: List<Generator<A>>) = intRange(0, generators.size - 1).bind { generators[it] }
|
||||
|
||||
fun <A : Any> success(generate: (SplittableRandom) -> A) = Generator { ErrorOr(generate(it)) }
|
||||
fun <A : Any> frequency(generators: List<Pair<Double, Generator<A>>>): Generator<A> {
|
||||
val ranges = mutableListOf<Pair<Double, Double>>()
|
||||
var current = 0.0
|
||||
generators.forEach {
|
||||
val next = current + it.first
|
||||
ranges.add(Pair(current, next))
|
||||
current = next
|
||||
}
|
||||
return doubleRange(0.0, current).bind { value ->
|
||||
generators[ranges.binarySearch { range ->
|
||||
if (value < range.first) {
|
||||
1
|
||||
} else if (value < range.second) {
|
||||
0
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}].second
|
||||
}
|
||||
}
|
||||
|
||||
fun <A : Any> sequence(generators: List<Generator<A>>) = Generator<List<A>> {
|
||||
val result = mutableListOf<A>()
|
||||
for (generator in generators) {
|
||||
val element = generator.generate(it)
|
||||
val v = element.value
|
||||
if (v != null) {
|
||||
result.add(v)
|
||||
} else {
|
||||
return@Generator ErrorOr.of(element.error!!)
|
||||
}
|
||||
}
|
||||
ErrorOr(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <A : Any> Generator.Companion.frequency(vararg generators: Pair<Double, Generator<A>>) = frequency(generators.toList())
|
||||
|
||||
fun <A : Any> Generator<A>.generateOrFail(random: SplittableRandom, numberOfTries: Int = 1): A {
|
||||
var error: Throwable? = null
|
||||
for (i in 0..numberOfTries - 1) {
|
||||
val result = generate(random)
|
||||
val v = result.value
|
||||
if (v != null) {
|
||||
return v
|
||||
} else {
|
||||
error = result.error
|
||||
}
|
||||
}
|
||||
if (error == null) {
|
||||
throw IllegalArgumentException("numberOfTries cannot be <= 0")
|
||||
} else {
|
||||
throw Exception("Failed to generate", error)
|
||||
}
|
||||
}
|
||||
|
||||
fun Generator.Companion.int() = Generator.success(SplittableRandom::nextInt)
|
||||
fun Generator.Companion.bytes(size: Int): Generator<ByteArray> = Generator.success { random ->
|
||||
ByteArray(size) { random.nextInt().toByte() }
|
||||
}
|
||||
|
||||
fun Generator.Companion.intRange(range: IntRange) = intRange(range.first, range.last)
|
||||
fun Generator.Companion.intRange(from: Int, to: Int): Generator<Int> = Generator.success {
|
||||
(from + Math.abs(it.nextInt()) % (to - from + 1)).toInt()
|
||||
}
|
||||
|
||||
fun Generator.Companion.longRange(range: LongRange) = longRange(range.first, range.last)
|
||||
fun Generator.Companion.longRange(from: Long, to: Long): Generator<Long> = Generator.success {
|
||||
(from + Math.abs(it.nextLong()) % (to - from + 1)).toLong()
|
||||
}
|
||||
|
||||
fun Generator.Companion.double() = Generator.success { it.nextDouble() }
|
||||
fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double> = Generator.success {
|
||||
from + it.nextDouble() * (to - from)
|
||||
}
|
||||
|
||||
fun <A : Any> Generator.Companion.replicate(number: Int, generator: Generator<A>): Generator<List<A>> {
|
||||
val generators = mutableListOf<Generator<A>>()
|
||||
for (i in 1..number) {
|
||||
generators.add(generator)
|
||||
}
|
||||
return sequence(generators)
|
||||
}
|
||||
|
||||
|
||||
fun <A : Any> Generator.Companion.replicatePoisson(meanSize: Double, generator: Generator<A>) = Generator<List<A>> {
|
||||
val chance = (meanSize - 1) / meanSize
|
||||
val result = mutableListOf<A>()
|
||||
var finish = false
|
||||
while (!finish) {
|
||||
val errorOr = Generator.doubleRange(0.0, 1.0).generate(it).bind { value ->
|
||||
if (value < chance) {
|
||||
generator.generate(it).map { result.add(it) }
|
||||
} else {
|
||||
finish = true
|
||||
ErrorOr(Unit)
|
||||
}
|
||||
}
|
||||
val e = errorOr.error
|
||||
if (e != null) {
|
||||
return@Generator ErrorOr.of(e)
|
||||
}
|
||||
}
|
||||
ErrorOr(result)
|
||||
}
|
||||
|
||||
fun <A : Any> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] }
|
||||
fun <A : Any> Generator.Companion.pickN(number: Int, list: List<A>) = Generator<List<A>> {
|
||||
val mask = BitSet(list.size)
|
||||
for (i in 0..Math.min(list.size, number) - 1) {
|
||||
mask[i] = 1
|
||||
}
|
||||
for (i in 0..mask.size() - 1) {
|
||||
val byte = mask[i]
|
||||
val swapIndex = i + it.nextInt(mask.size() - i)
|
||||
mask[i] = mask[swapIndex]
|
||||
mask[swapIndex] = byte
|
||||
}
|
||||
val resultList = ArrayList<A>()
|
||||
list.forEachIndexed { index, a ->
|
||||
if (mask[index]) {
|
||||
resultList.add(a)
|
||||
}
|
||||
}
|
||||
ErrorOr(resultList)
|
||||
}
|
||||
|
||||
fun <A> Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) =
|
||||
sampleBernoulli(listOf(collection), maxRatio)
|
||||
|
||||
fun <A> Generator.Companion.sampleBernoulli(collection: Collection<A>, maxRatio: Double = 1.0): Generator<List<A>> =
|
||||
intRange(0, (maxRatio * collection.size).toInt()).bind { howMany ->
|
||||
replicate(collection.size, Generator.doubleRange(0.0, 1.0)).map { chances ->
|
||||
val result = mutableListOf<A>()
|
||||
collection.forEachIndexed { index, element ->
|
||||
if (chances[index] < howMany.toDouble() / collection.size.toDouble()) {
|
||||
result.add(element)
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
21
client/src/main/kotlin/net/corda/client/mock/Generators.kt
Normal file
21
client/src/main/kotlin/net/corda/client/mock/Generators.kt
Normal file
@ -0,0 +1,21 @@
|
||||
package net.corda.client.mock
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import java.util.*
|
||||
|
||||
fun generateCurrency(): Generator<Currency> {
|
||||
return Generator.pickOne(Currency.getAvailableCurrencies().toList())
|
||||
}
|
||||
|
||||
fun <T : Any> generateAmount(min: Long, max: Long, tokenGenerator: Generator<T>): Generator<Amount<T>> {
|
||||
return Generator.longRange(min, max).combine(tokenGenerator) { quantity, token -> Amount(quantity, token) }
|
||||
}
|
||||
|
||||
fun generateCurrencyAmount(min: Long, max: Long): Generator<Amount<Currency>> {
|
||||
return generateAmount(min, max, generateCurrency())
|
||||
}
|
||||
|
||||
fun generateIssueRef(size: Int): Generator<OpaqueBytes> {
|
||||
return Generator.bytes(size).map { OpaqueBytes(it) }
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import net.corda.client.fxutils.foldToObservableList
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.node.services.Vault
|
||||
import rx.Observable
|
||||
|
||||
data class Diff<out T : ContractState>(
|
||||
val added: Collection<StateAndRef<T>>,
|
||||
val removed: Collection<StateRef>
|
||||
)
|
||||
|
||||
/**
|
||||
* This model exposes the list of owned contract states.
|
||||
*/
|
||||
class ContractStateModel {
|
||||
private val vaultUpdates: Observable<Vault.Update> by observable(NodeMonitorModel::vaultUpdates)
|
||||
|
||||
private val contractStatesDiff: Observable<Diff<ContractState>> = vaultUpdates.map {
|
||||
Diff(it.produced, it.consumed)
|
||||
}
|
||||
private val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
|
||||
// We can't filter removed hashes here as we don't have type info
|
||||
Diff(it.added.filterCashStateAndRefs(), it.removed)
|
||||
}
|
||||
val cashStates: ObservableList<StateAndRef<Cash.State>> =
|
||||
cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
|
||||
observableList.removeIf { it.ref in statesDiff.removed }
|
||||
observableList.addAll(statesDiff.added)
|
||||
}
|
||||
|
||||
val cash = cashStates.map { it.state.data.amount }
|
||||
|
||||
companion object {
|
||||
private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> {
|
||||
return this.map { stateAndRef ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (stateAndRef.state.data is Cash.State) {
|
||||
// Kotlin doesn't unify here for some reason
|
||||
stateAndRef as StateAndRef<Cash.State>
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +1,18 @@
|
||||
package com.r3corda.client.model
|
||||
package net.corda.client.model
|
||||
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import net.corda.core.contracts.Amount
|
||||
import java.util.*
|
||||
|
||||
|
||||
interface ExchangeRate {
|
||||
fun rate(from: Currency, to: Currency): Double
|
||||
}
|
||||
|
||||
fun ExchangeRate.exchangeAmount(amount: Amount<Currency>, to: Currency) =
|
||||
Amount(exchangeDouble(amount, to).toLong(), to)
|
||||
|
||||
fun ExchangeRate.exchangeDouble(amount: Amount<Currency>, to: Currency) =
|
||||
rate(amount.token, to) * amount.quantity
|
||||
|
@ -0,0 +1,115 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import net.corda.client.fxutils.*
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.messaging.StateMachineUpdate
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
|
||||
data class GatheredTransactionData(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
val stateMachines: ObservableList<out StateMachineData>
|
||||
)
|
||||
|
||||
/**
|
||||
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
|
||||
* to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
|
||||
* because of permissioning)
|
||||
*/
|
||||
data class PartiallyResolvedTransaction(
|
||||
val transaction: SignedTransaction,
|
||||
val inputs: List<ObservableValue<InputResolution>>) {
|
||||
val id = transaction.id
|
||||
|
||||
sealed class InputResolution(val stateRef: StateRef) {
|
||||
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
|
||||
class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSignedTransaction(
|
||||
transaction: SignedTransaction,
|
||||
transactions: ObservableMap<SecureHash, SignedTransaction>
|
||||
) = PartiallyResolvedTransaction(
|
||||
transaction = transaction,
|
||||
inputs = transaction.tx.inputs.map { stateRef ->
|
||||
EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
|
||||
if (it == null) {
|
||||
InputResolution.Unresolved(stateRef)
|
||||
} else {
|
||||
InputResolution.Resolved(it.tx.outRef(stateRef.index))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionCreateStatus(val message: String?) {
|
||||
class Started(message: String?) : TransactionCreateStatus(message)
|
||||
class Failed(message: String?) : TransactionCreateStatus(message)
|
||||
|
||||
override fun toString(): String = message ?: javaClass.simpleName
|
||||
}
|
||||
|
||||
data class FlowStatus(
|
||||
val status: String
|
||||
)
|
||||
|
||||
sealed class StateMachineStatus(val stateMachineName: String) {
|
||||
class Added(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
class Removed(stateMachineName: String) : StateMachineStatus(stateMachineName)
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
|
||||
}
|
||||
|
||||
data class StateMachineData(
|
||||
val id: StateMachineRunId,
|
||||
val flowStatus: ObservableValue<FlowStatus?>,
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus>
|
||||
)
|
||||
|
||||
/**
|
||||
* This model provides an observable list of transactions and what state machines/flows recorded them
|
||||
*/
|
||||
class GatheredTransactionDataModel {
|
||||
private val transactions by observable(NodeMonitorModel::transactions)
|
||||
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
|
||||
private val progressTracking by observable(NodeMonitorModel::progressTracking)
|
||||
private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
|
||||
|
||||
private val collectedTransactions = transactions.recordInSequence()
|
||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
|
||||
private val stateMachineStatus = stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
|
||||
map[update.id] = added
|
||||
}
|
||||
is StateMachineUpdate.Removed -> {
|
||||
val added = map[update.id]
|
||||
added ?: throw Exception("State machine removed with unknown id ${update.id}")
|
||||
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
|
||||
}
|
||||
}
|
||||
}
|
||||
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
|
||||
}.getObservableValues()
|
||||
// TODO : Create a new screen for state machines.
|
||||
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.model
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.beans.property.ObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
@ -106,7 +106,7 @@ object Models {
|
||||
private val dependencyGraph = HashMap<KClass<*>, MutableSet<KClass<*>>>()
|
||||
|
||||
fun <M : Any> initModel(klass: KClass<M>) = modelStore.getOrPut(klass) { klass.java.newInstance() }
|
||||
fun <M : Any> get(klass: KClass<M>, origin: KClass<*>) : M {
|
||||
fun <M : Any> get(klass: KClass<M>, origin: KClass<*>): M {
|
||||
dependencyGraph.getOrPut(origin) { mutableSetOf<KClass<*>>() }.add(klass)
|
||||
val model = initModel(klass)
|
||||
if (model.javaClass != klass.java) {
|
||||
@ -116,57 +116,69 @@ object Models {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return model as M
|
||||
}
|
||||
inline fun <reified M : Any> get(origin: KClass<*>) : M = get(M::class, origin)
|
||||
|
||||
inline fun <reified M : Any> get(origin: KClass<*>): M = get(M::class, origin)
|
||||
}
|
||||
|
||||
sealed class TrackedDelegate<M : Any>(val klass: KClass<M>) {
|
||||
init { Models.initModel(klass) }
|
||||
init {
|
||||
Models.initModel(klass)
|
||||
}
|
||||
|
||||
class ObservableDelegate<M : Any, T> (klass: KClass<M>, val observableProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
|
||||
class ObservableDelegate<M : Any, T>(klass: KClass<M>, val observableProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): Observable<T> {
|
||||
return observableProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
class ObserverDelegate<M : Any, T> (klass: KClass<M>, val observerProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
|
||||
|
||||
class ObserverDelegate<M : Any, T>(klass: KClass<M>, val observerProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): Observer<T> {
|
||||
return observerProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
class SubjectDelegate<M : Any, T> (klass: KClass<M>, val subjectProperty: (M) -> Subject<T, T>) : TrackedDelegate<M>(klass) {
|
||||
|
||||
class SubjectDelegate<M : Any, T>(klass: KClass<M>, val subjectProperty: (M) -> Subject<T, T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): Subject<T, T> {
|
||||
return subjectProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
class EventStreamDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> org.reactfx.EventStream<T>) : TrackedDelegate<M>(klass) {
|
||||
|
||||
class EventStreamDelegate<M : Any, T>(klass: KClass<M>, val eventStreamProperty: (M) -> org.reactfx.EventStream<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): org.reactfx.EventStream<T> {
|
||||
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
class EventSinkDelegate<M : Any, T> (klass: KClass<M>, val eventSinkProperty: (M) -> org.reactfx.EventSink<T>) : TrackedDelegate<M>(klass) {
|
||||
|
||||
class EventSinkDelegate<M : Any, T>(klass: KClass<M>, val eventSinkProperty: (M) -> org.reactfx.EventSink<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): org.reactfx.EventSink<T> {
|
||||
return eventSinkProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableValueDelegate<M : Any, T>(klass: KClass<M>, val observableValueProperty: (M) -> ObservableValue<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableValue<T> {
|
||||
return observableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
|
||||
class WritableValueDelegate<M : Any, T>(klass: KClass<M>, val writableValueProperty: (M) -> WritableValue<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): WritableValue<T> {
|
||||
return writableValueProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableListDelegate<M : Any, T>(klass: KClass<M>, val observableListProperty: (M) -> ObservableList<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<T> {
|
||||
return observableListProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
|
||||
class ObservableListReadOnlyDelegate<M : Any, out T>(klass: KClass<M>, val observableListReadOnlyProperty: (M) -> ObservableList<out T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObservableList<out T> {
|
||||
return observableListReadOnlyProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||
}
|
||||
}
|
||||
|
||||
class ObjectPropertyDelegate<M : Any, T>(klass: KClass<M>, val objectPropertyProperty: (M) -> ObjectProperty<T>) : TrackedDelegate<M>(klass) {
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): ObjectProperty<T> {
|
||||
return objectPropertyProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
@ -0,0 +1,49 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import net.corda.client.fxutils.firstOrDefault
|
||||
import net.corda.client.fxutils.firstOrNullObservable
|
||||
import net.corda.client.fxutils.foldToObservableList
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import java.security.PublicKey
|
||||
|
||||
class NetworkIdentityModel {
|
||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
||||
observableList.removeIf {
|
||||
when (update) {
|
||||
is MapChange.Removed -> it == update.node
|
||||
is MapChange.Modified -> it == update.previousNode
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
observableList.addAll(update.node)
|
||||
}
|
||||
|
||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||
|
||||
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { !it.isCordaService() }
|
||||
val notaries: ObservableList<NodeInfo> = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } }
|
||||
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
|
||||
|
||||
private fun NodeInfo.isCordaService(): Boolean {
|
||||
// TODO: better way to identify Corda service?
|
||||
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
|
||||
}
|
||||
|
||||
fun lookup(compositeKey: CompositeKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey == compositeKey }) {
|
||||
it.legalIdentity.owningKey == compositeKey
|
||||
}
|
||||
|
||||
fun lookup(publicKey: PublicKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey.keys.any { it == publicKey } }) {
|
||||
it.legalIdentity.owningKey.keys.any { it == publicKey }
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package net.corda.client.model
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import net.corda.client.CordaRPCClient
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.node.services.config.NodeSSLConfiguration
|
||||
import net.corda.node.services.messaging.CordaRPCOps
|
||||
import net.corda.node.services.messaging.StateMachineInfo
|
||||
import net.corda.node.services.messaging.StateMachineUpdate
|
||||
import net.corda.node.services.messaging.startFlow
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
|
||||
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
|
||||
companion object {
|
||||
fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable<ProgressTrackingEvent>? {
|
||||
return stateMachine.progressTrackerStepAndUpdates?.let { pair ->
|
||||
val (current, future) = pair
|
||||
future.map { ProgressTrackingEvent(stateMachine.id, it) }.startWith(ProgressTrackingEvent(stateMachine.id, current))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This model exposes raw event streams to and from the node.
|
||||
*/
|
||||
class NodeMonitorModel {
|
||||
|
||||
private val stateMachineUpdatesSubject = PublishSubject.create<StateMachineUpdate>()
|
||||
private val vaultUpdatesSubject = PublishSubject.create<Vault.Update>()
|
||||
private val transactionsSubject = PublishSubject.create<SignedTransaction>()
|
||||
private val stateMachineTransactionMappingSubject = PublishSubject.create<StateMachineTransactionMapping>()
|
||||
private val progressTrackingSubject = PublishSubject.create<ProgressTrackingEvent>()
|
||||
private val networkMapSubject = PublishSubject.create<MapChange>()
|
||||
|
||||
val stateMachineUpdates: Observable<StateMachineUpdate> = stateMachineUpdatesSubject
|
||||
val vaultUpdates: Observable<Vault.Update> = vaultUpdatesSubject
|
||||
val transactions: Observable<SignedTransaction> = transactionsSubject
|
||||
val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> = stateMachineTransactionMappingSubject
|
||||
val progressTracking: Observable<ProgressTrackingEvent> = progressTrackingSubject
|
||||
val networkMap: Observable<MapChange> = networkMapSubject
|
||||
|
||||
private val clientToServiceSource = PublishSubject.create<CashCommand>()
|
||||
val clientToService: PublishSubject<CashCommand> = clientToServiceSource
|
||||
|
||||
val proxyObservable = SimpleObjectProperty<CordaRPCOps?>()
|
||||
|
||||
/**
|
||||
* Register for updates to/from a given vault.
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(nodeHostAndPort: HostAndPort, sslConfig: NodeSSLConfiguration, username: String, password: String) {
|
||||
val client = CordaRPCClient(nodeHostAndPort, sslConfig)
|
||||
client.start(username, password)
|
||||
val proxy = client.proxy()
|
||||
|
||||
val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates()
|
||||
// Extract the flow tracking stream
|
||||
// TODO is there a nicer way of doing this? Stream of streams in general results in code like this...
|
||||
val currentProgressTrackerUpdates = stateMachines.mapNotNull { stateMachine ->
|
||||
ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachine)
|
||||
}
|
||||
val futureProgressTrackerUpdates = stateMachineUpdatesSubject.map { stateMachineUpdate ->
|
||||
if (stateMachineUpdate is StateMachineUpdate.Added) {
|
||||
ProgressTrackingEvent.createStreamFromStateMachineInfo(stateMachineUpdate.stateMachineInfo) ?: Observable.empty<ProgressTrackingEvent>()
|
||||
} else {
|
||||
Observable.empty<ProgressTrackingEvent>()
|
||||
}
|
||||
}
|
||||
futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.subscribe(progressTrackingSubject)
|
||||
|
||||
// Now the state machines
|
||||
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }
|
||||
stateMachineUpdates.startWith(currentStateMachines).subscribe(stateMachineUpdatesSubject)
|
||||
|
||||
// Vault updates
|
||||
val (vault, vaultUpdates) = proxy.vaultAndUpdates()
|
||||
val initialVaultUpdate = Vault.Update(setOf(), vault.toSet())
|
||||
vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject)
|
||||
|
||||
// Transactions
|
||||
val (transactions, newTransactions) = proxy.verifiedTransactions()
|
||||
newTransactions.startWith(transactions).subscribe(transactionsSubject)
|
||||
|
||||
// SM -> TX mapping
|
||||
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMapping()
|
||||
futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject)
|
||||
|
||||
// Parties on network
|
||||
val (parties, futurePartyUpdate) = proxy.networkMapUpdates()
|
||||
futurePartyUpdate.startWith(parties.map { MapChange.Added(it) }).subscribe(networkMapSubject)
|
||||
|
||||
// Client -> Service
|
||||
clientToServiceSource.subscribe {
|
||||
proxy.startFlow(::CashFlow, it)
|
||||
}
|
||||
proxyObservable.set(proxy)
|
||||
}
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
package com.r3corda.client
|
||||
|
||||
import com.r3corda.client.impl.CordaRPCClientImpl
|
||||
import com.r3corda.core.serialization.SerializedBytes
|
||||
import com.r3corda.core.utilities.LogHelper
|
||||
import com.r3corda.node.services.messaging.*
|
||||
import com.r3corda.node.utilities.AffinityExecutor
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
|
||||
import org.apache.activemq.artemis.api.core.client.ClientProducer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
||||
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
|
||||
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ClientRPCInfrastructureTests {
|
||||
// TODO: Test that timeouts work
|
||||
|
||||
lateinit var artemis: EmbeddedActiveMQ
|
||||
lateinit var serverSession: ClientSession
|
||||
lateinit var clientSession: ClientSession
|
||||
lateinit var producer: ClientProducer
|
||||
lateinit var serverThread: AffinityExecutor.ServiceAffinityExecutor
|
||||
lateinit var proxy: ITestOps
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// Set up an in-memory Artemis with an RPC requests queue.
|
||||
artemis = EmbeddedActiveMQ()
|
||||
artemis.setConfiguration(ConfigurationImpl().apply {
|
||||
acceptorConfigurations = setOf(TransportConfiguration(InVMAcceptorFactory::class.java.name))
|
||||
isSecurityEnabled = false
|
||||
isPersistenceEnabled = false
|
||||
})
|
||||
artemis.start()
|
||||
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(TransportConfiguration(InVMConnectorFactory::class.java.name))
|
||||
val sessionFactory = serverLocator.createSessionFactory()
|
||||
|
||||
serverSession = sessionFactory.createSession()
|
||||
serverSession.start()
|
||||
serverSession.createTemporaryQueue(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE, ArtemisMessagingComponent.RPC_REQUESTS_QUEUE)
|
||||
producer = serverSession.createProducer()
|
||||
val dispatcher = object : RPCDispatcher(TestOps()) {
|
||||
override fun send(bits: SerializedBytes<*>, toAddress: String) {
|
||||
val msg = serverSession.createMessage(false).apply {
|
||||
writeBodyBufferBytes(bits.bits)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
}
|
||||
producer.send(toAddress, msg)
|
||||
}
|
||||
}
|
||||
serverThread = AffinityExecutor.ServiceAffinityExecutor("unit-tests-rpc-dispatch-thread", 1)
|
||||
val serverConsumer = serverSession.createConsumer(ArtemisMessagingComponent.RPC_REQUESTS_QUEUE)
|
||||
serverSession.createTemporaryQueue("activemq.notifications", "rpc.qremovals", "_AMQ_NotifType = 'BINDING_REMOVED'")
|
||||
val serverNotifConsumer = serverSession.createConsumer("rpc.qremovals")
|
||||
dispatcher.start(serverConsumer, serverNotifConsumer, serverThread)
|
||||
|
||||
clientSession = sessionFactory.createSession()
|
||||
clientSession.start()
|
||||
|
||||
LogHelper.setLevel("+com.r3corda.rpc"/*, "+org.apache.activemq"*/)
|
||||
|
||||
proxy = CordaRPCClientImpl(clientSession, ReentrantLock(), "tests").proxyFor(ITestOps::class.java)
|
||||
}
|
||||
|
||||
@After
|
||||
fun shutdown() {
|
||||
(proxy as Closeable).close()
|
||||
clientSession.stop()
|
||||
serverSession.stop()
|
||||
artemis.stop()
|
||||
serverThread.shutdownNow()
|
||||
}
|
||||
|
||||
interface ITestOps : RPCOps {
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun barf()
|
||||
|
||||
fun void()
|
||||
|
||||
fun someCalculation(str: String, num: Int): String
|
||||
|
||||
@RPCReturnsObservables
|
||||
fun makeObservable(): Observable<Int>
|
||||
|
||||
@RPCReturnsObservables
|
||||
fun makeComplicatedObservable(): Observable<Pair<String, Observable<String>>>
|
||||
|
||||
@RPCSinceVersion(2)
|
||||
fun addedLater()
|
||||
}
|
||||
|
||||
lateinit var complicatedObservable: Observable<Pair<String, Observable<String>>>
|
||||
|
||||
inner class TestOps : ITestOps {
|
||||
override val protocolVersion = 1
|
||||
|
||||
override fun barf() {
|
||||
throw IllegalArgumentException("Barf!")
|
||||
}
|
||||
|
||||
override fun void() { }
|
||||
|
||||
override fun someCalculation(str: String, num: Int) = "$str $num"
|
||||
|
||||
override fun makeObservable(): Observable<Int> {
|
||||
return Observable.just(1, 2, 3, 4)
|
||||
}
|
||||
|
||||
override fun makeComplicatedObservable() = complicatedObservable
|
||||
|
||||
override fun addedLater() {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun simpleRPCs() {
|
||||
// Does nothing, doesn't throw.
|
||||
proxy.void()
|
||||
|
||||
assertEquals("Barf!", assertFailsWith<IllegalArgumentException> {
|
||||
proxy.barf()
|
||||
}.message)
|
||||
|
||||
assertEquals("hi 5", proxy.someCalculation("hi", 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun simpleObservable() {
|
||||
// This tests that the observations are transmitted correctly, also completion is transmitted.
|
||||
val observations = proxy.makeObservable().toBlocking().toIterable().toList()
|
||||
assertEquals(listOf(1, 2, 3, 4), observations)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun complexObservables() {
|
||||
// This checks that we can return an object graph with complex usage of observables, like an observable
|
||||
// that emits objects that contain more observables.
|
||||
val serverQuotes = PublishSubject.create<Pair<String, Observable<String>>>()
|
||||
val unsubscribeLatch = CountDownLatch(1)
|
||||
complicatedObservable = serverQuotes.asObservable().doOnUnsubscribe { unsubscribeLatch.countDown() }
|
||||
|
||||
val twainQuotes = "Mark Twain" to Observable.just(
|
||||
"I have never let my schooling interfere with my education.",
|
||||
"Clothes make the man. Naked people have little or no influence on society."
|
||||
)
|
||||
val wildeQuotes = "Oscar Wilde" to Observable.just(
|
||||
"I can resist everything except temptation.",
|
||||
"Always forgive your enemies - nothing annoys them so much."
|
||||
)
|
||||
|
||||
val clientQuotes = LinkedBlockingQueue<String>()
|
||||
val clientObs = proxy.makeComplicatedObservable()
|
||||
|
||||
val subscription = clientObs.subscribe {
|
||||
val name = it.first
|
||||
it.second.subscribe {
|
||||
clientQuotes += "Quote by $name: $it"
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(1, clientSession.addressQuery(SimpleString("tests.rpc.observations.#")).queueNames.size)
|
||||
|
||||
assertThat(clientQuotes).isEmpty()
|
||||
|
||||
serverQuotes.onNext(twainQuotes)
|
||||
assertEquals("Quote by Mark Twain: I have never let my schooling interfere with my education.", clientQuotes.take())
|
||||
assertEquals("Quote by Mark Twain: Clothes make the man. Naked people have little or no influence on society.", clientQuotes.take())
|
||||
|
||||
serverQuotes.onNext(wildeQuotes)
|
||||
assertEquals("Quote by Oscar Wilde: I can resist everything except temptation.", clientQuotes.take())
|
||||
assertEquals("Quote by Oscar Wilde: Always forgive your enemies - nothing annoys them so much.", clientQuotes.take())
|
||||
|
||||
assertTrue(serverQuotes.hasObservers())
|
||||
subscription.unsubscribe()
|
||||
unsubscribeLatch.await()
|
||||
assertEquals(0, clientSession.addressQuery(SimpleString("tests.rpc.observations.#")).queueNames.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun versioning() {
|
||||
assertFailsWith<UnsupportedOperationException> { proxy.addedLater() }
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package net.corda.client
|
||||
|
||||
import net.corda.client.impl.CordaRPCClientImpl
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.LogHelper
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.messaging.*
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
|
||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientProducer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
||||
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMConnectorFactory
|
||||
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ClientRPCInfrastructureTests {
|
||||
// TODO: Test that timeouts work
|
||||
|
||||
lateinit var artemis: EmbeddedActiveMQ
|
||||
lateinit var serverSession: ClientSession
|
||||
lateinit var clientSession: ClientSession
|
||||
lateinit var producer: ClientProducer
|
||||
lateinit var serverThread: AffinityExecutor.ServiceAffinityExecutor
|
||||
lateinit var proxy: TestOps
|
||||
|
||||
private val authenticatedUser = User("test", "password", permissions = setOf())
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// Set up an in-memory Artemis with an RPC requests queue.
|
||||
artemis = EmbeddedActiveMQ()
|
||||
artemis.setConfiguration(ConfigurationImpl().apply {
|
||||
acceptorConfigurations = setOf(TransportConfiguration(InVMAcceptorFactory::class.java.name))
|
||||
isSecurityEnabled = false
|
||||
isPersistenceEnabled = false
|
||||
})
|
||||
artemis.start()
|
||||
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(TransportConfiguration(InVMConnectorFactory::class.java.name))
|
||||
val sessionFactory = serverLocator.createSessionFactory()
|
||||
|
||||
serverSession = sessionFactory.createSession()
|
||||
serverSession.start()
|
||||
serverSession.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE)
|
||||
producer = serverSession.createProducer()
|
||||
val userService = object : RPCUserService {
|
||||
override fun getUser(username: String): User? = throw UnsupportedOperationException()
|
||||
override val users: List<User> get() = throw UnsupportedOperationException()
|
||||
}
|
||||
val dispatcher = object : RPCDispatcher(TestOpsImpl(), userService) {
|
||||
override fun send(data: SerializedBytes<*>, toAddress: String) {
|
||||
val msg = serverSession.createMessage(false).apply {
|
||||
writeBodyBufferBytes(data.bytes)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
}
|
||||
producer.send(toAddress, msg)
|
||||
}
|
||||
|
||||
override fun getUser(message: ClientMessage): User = authenticatedUser
|
||||
}
|
||||
serverThread = AffinityExecutor.ServiceAffinityExecutor("unit-tests-rpc-dispatch-thread", 1)
|
||||
val serverConsumer = serverSession.createConsumer(RPC_REQUESTS_QUEUE)
|
||||
serverSession.createTemporaryQueue("activemq.notifications", "rpc.qremovals", "_AMQ_NotifType = 'BINDING_REMOVED'")
|
||||
val serverNotifConsumer = serverSession.createConsumer("rpc.qremovals")
|
||||
dispatcher.start(serverConsumer, serverNotifConsumer, serverThread)
|
||||
|
||||
clientSession = sessionFactory.createSession()
|
||||
clientSession.start()
|
||||
|
||||
LogHelper.setLevel("+net.corda.rpc")
|
||||
|
||||
proxy = CordaRPCClientImpl(clientSession, ReentrantLock(), authenticatedUser.username).proxyFor(TestOps::class.java)
|
||||
}
|
||||
|
||||
@After
|
||||
fun shutdown() {
|
||||
(proxy as Closeable?)?.close()
|
||||
clientSession.stop()
|
||||
serverSession.stop()
|
||||
artemis.stop()
|
||||
serverThread.shutdownNow()
|
||||
}
|
||||
|
||||
interface TestOps : RPCOps {
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun barf()
|
||||
|
||||
fun void()
|
||||
|
||||
fun someCalculation(str: String, num: Int): String
|
||||
|
||||
@RPCReturnsObservables
|
||||
fun makeObservable(): Observable<Int>
|
||||
|
||||
@RPCReturnsObservables
|
||||
fun makeComplicatedObservable(): Observable<Pair<String, Observable<String>>>
|
||||
|
||||
@RPCSinceVersion(2)
|
||||
fun addedLater()
|
||||
|
||||
fun captureUser(): String
|
||||
}
|
||||
|
||||
lateinit var complicatedObservable: Observable<Pair<String, Observable<String>>>
|
||||
|
||||
inner class TestOpsImpl : TestOps {
|
||||
override val protocolVersion = 1
|
||||
|
||||
override fun barf(): Unit = throw IllegalArgumentException("Barf!")
|
||||
|
||||
override fun void() {
|
||||
}
|
||||
|
||||
override fun someCalculation(str: String, num: Int) = "$str $num"
|
||||
|
||||
override fun makeObservable(): Observable<Int> = Observable.just(1, 2, 3, 4)
|
||||
|
||||
override fun makeComplicatedObservable() = complicatedObservable
|
||||
|
||||
override fun addedLater(): Unit = throw UnsupportedOperationException("not implemented")
|
||||
|
||||
override fun captureUser(): String = CURRENT_RPC_USER.get().username
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `simple RPCs`() {
|
||||
// Does nothing, doesn't throw.
|
||||
proxy.void()
|
||||
|
||||
assertEquals("Barf!", assertFailsWith<IllegalArgumentException> {
|
||||
proxy.barf()
|
||||
}.message)
|
||||
|
||||
assertEquals("hi 5", proxy.someCalculation("hi", 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `simple observable`() {
|
||||
// This tests that the observations are transmitted correctly, also completion is transmitted.
|
||||
val observations = proxy.makeObservable().toBlocking().toIterable().toList()
|
||||
assertEquals(listOf(1, 2, 3, 4), observations)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `complex observables`() {
|
||||
// This checks that we can return an object graph with complex usage of observables, like an observable
|
||||
// that emits objects that contain more observables.
|
||||
val serverQuotes = PublishSubject.create<Pair<String, Observable<String>>>()
|
||||
val unsubscribeLatch = CountDownLatch(1)
|
||||
complicatedObservable = serverQuotes.asObservable().doOnUnsubscribe { unsubscribeLatch.countDown() }
|
||||
|
||||
val twainQuotes = "Mark Twain" to Observable.just(
|
||||
"I have never let my schooling interfere with my education.",
|
||||
"Clothes make the man. Naked people have little or no influence on society."
|
||||
)
|
||||
val wildeQuotes = "Oscar Wilde" to Observable.just(
|
||||
"I can resist everything except temptation.",
|
||||
"Always forgive your enemies - nothing annoys them so much."
|
||||
)
|
||||
|
||||
val clientQuotes = LinkedBlockingQueue<String>()
|
||||
val clientObs = proxy.makeComplicatedObservable()
|
||||
|
||||
val subscription = clientObs.subscribe {
|
||||
val name = it.first
|
||||
it.second.subscribe {
|
||||
clientQuotes += "Quote by $name: $it"
|
||||
}
|
||||
}
|
||||
|
||||
val rpcQueuesQuery = SimpleString("clients.${authenticatedUser.username}.rpc.*")
|
||||
assertEquals(2, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
|
||||
|
||||
assertThat(clientQuotes).isEmpty()
|
||||
|
||||
serverQuotes.onNext(twainQuotes)
|
||||
assertEquals("Quote by Mark Twain: I have never let my schooling interfere with my education.", clientQuotes.take())
|
||||
assertEquals("Quote by Mark Twain: Clothes make the man. Naked people have little or no influence on society.", clientQuotes.take())
|
||||
|
||||
serverQuotes.onNext(wildeQuotes)
|
||||
assertEquals("Quote by Oscar Wilde: I can resist everything except temptation.", clientQuotes.take())
|
||||
assertEquals("Quote by Oscar Wilde: Always forgive your enemies - nothing annoys them so much.", clientQuotes.take())
|
||||
|
||||
assertTrue(serverQuotes.hasObservers())
|
||||
subscription.unsubscribe()
|
||||
unsubscribeLatch.await()
|
||||
assertEquals(1, clientSession.addressQuery(rpcQueuesQuery).queueNames.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun versioning() {
|
||||
assertFailsWith<UnsupportedOperationException> { proxy.addedLater() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `authenticated user is available to RPC`() {
|
||||
assertThat(proxy.captureUser()).isEqualTo(authenticatedUser.username)
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
@ -24,7 +24,7 @@ class ConcatenatedListTest {
|
||||
fun <A> ConcatenatedList<A>.checkInvariants() {
|
||||
assertEquals(nestedIndexOffsets.size, source.size)
|
||||
var currentOffset = 0
|
||||
for (i in 0 .. source.size - 1) {
|
||||
for (i in 0..source.size - 1) {
|
||||
currentOffset += source[i].size
|
||||
assertEquals(nestedIndexOffsets[i], currentOffset)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.collections.FXCollections
|
@ -1,10 +1,9 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class LeftOuterJoinedMapTest {
|
||||
@ -23,7 +22,7 @@ class LeftOuterJoinedMapTest {
|
||||
dogs = FXCollections.observableArrayList<Dog>(Dog("Scruffy", owner = "Bob"))
|
||||
joinedList = people.leftOuterJoin(dogs, Person::name, Dog::owner) { person, dogs -> Pair(person, dogs) }
|
||||
// We replay the nested observable as well
|
||||
replayedList = ReplayedList(joinedList.map { Pair(it.first, ReplayedList(it.second)) })
|
||||
replayedList = ReplayedList(joinedList.map { Pair(it.first, ReplayedList(it.second)) })
|
||||
}
|
||||
|
||||
// TODO perhaps these are too brittle because they test indices that are not stable. Use Expect dsl?
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import org.junit.Before
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.client.fxutils
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.collections.MapChangeListener
|
||||
import javafx.collections.ObservableMap
|
30
config/dev/jolokia-access.xml
Normal file
30
config/dev/jolokia-access.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<restrict>
|
||||
<http>
|
||||
<method>post</method>
|
||||
<method>get</method>
|
||||
</http>
|
||||
|
||||
<commands>
|
||||
<command>read</command>
|
||||
<command>list</command>
|
||||
</commands>
|
||||
|
||||
<!-- allow anyone to force a garbage collection -->
|
||||
<allow>
|
||||
<mbean>
|
||||
<name>java.lang:type=Memory</name>
|
||||
<operation>gc</operation>
|
||||
</mbean>
|
||||
</allow>
|
||||
|
||||
<!-- in case we ever end up using c3pio connection pooling, this example from the docs prevents the password being exported -->
|
||||
<deny>
|
||||
<mbean>
|
||||
<name>com.mchange.v2.c3p0:type=PooledDataSource,*</name>
|
||||
<attribute>properties</attribute>
|
||||
</mbean>
|
||||
</deny>
|
||||
|
||||
</restrict>
|
@ -4,7 +4,8 @@
|
||||
<Properties>
|
||||
<Property name="log-path">logs</Property>
|
||||
<Property name="log-name">node-${hostName}</Property>
|
||||
<Property name="archive">${log-path}/archive</Property>
|
||||
<Property name="archive">${sys:log-path}/archive</Property>
|
||||
<Property name="consoleLogLevel">error</Property>
|
||||
</Properties>
|
||||
|
||||
<ThresholdFilter level="trace"/>
|
||||
@ -21,7 +22,7 @@
|
||||
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
||||
those that are older than 60 days, but keep the most recent 10 GB -->
|
||||
<RollingFile name="RollingFile-Appender"
|
||||
fileName="${log-path}/${log-name}.log"
|
||||
fileName="${sys:log-path}/${log-name}.log"
|
||||
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
|
||||
|
||||
<PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{1} - %msg%n"/>
|
||||
@ -47,13 +48,12 @@
|
||||
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
||||
<AppenderRef ref="RollingFile-Appender" level="info"/>
|
||||
</Root>
|
||||
<Logger name="com.r3corda" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
<Logger name="net.corda" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
@ -13,8 +13,8 @@
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Root>
|
||||
<Logger name="com.r3corda" level="info" additivity="false">
|
||||
<Logger name="net.corda" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
</Configuration>
|
||||
|
@ -1,42 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: CanonicalizerPlugin
|
||||
apply plugin: DefaultPublishTasks
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
|
||||
testCompile project(':test-utils')
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
resources {
|
||||
srcDir "../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
contracts(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'contracts'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package com.r3corda.contracts.isolated
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
|
||||
val ANOTHER_DUMMY_PROGRAM_ID = AnotherDummyContract()
|
||||
|
||||
class AnotherDummyContract : Contract, com.r3corda.core.node.DummyContractBackdoor {
|
||||
data class State(val magicNumber: Int = 0) : ContractState {
|
||||
override val contract = ANOTHER_DUMMY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Create : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
// 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 TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey))
|
||||
}
|
||||
|
||||
override fun inspectState(state: ContractState): Int = (state as State).magicNumber
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package com.r3corda.core.node
|
||||
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.PartyAndReference
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.crypto.Party
|
||||
|
||||
interface DummyContractBackdoor {
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder
|
||||
|
||||
fun inspectState(state: ContractState): Int
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package com.r3corda.contracts.testing
|
||||
|
||||
import com.pholser.junit.quickcheck.generator.GenerationStatus
|
||||
import com.pholser.junit.quickcheck.generator.Generator
|
||||
import com.pholser.junit.quickcheck.generator.java.util.ArrayListGenerator
|
||||
import com.pholser.junit.quickcheck.random.SourceOfRandomness
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.NullSignature
|
||||
import com.r3corda.core.testing.*
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
|
||||
/**
|
||||
* This file contains generators for quickcheck style testing. The idea is that we can write random instance generators
|
||||
* for each type we have in the code and test against those instead of predefined mock data. This style of testing can
|
||||
* catch corner case bugs and test algebraic properties of the code, for example deserialize(serialize(generatedThing)) == generatedThing
|
||||
*
|
||||
* TODO add combinators for easier Generator writing
|
||||
*/
|
||||
class ContractStateGenerator : Generator<ContractState>(ContractState::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): ContractState {
|
||||
return Cash.State(
|
||||
amount = AmountGenerator(IssuedGenerator(CurrencyGenerator())).generate(random, status),
|
||||
owner = PublicKeyGenerator().generate(random, status)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class MoveGenerator : Generator<Cash.Commands.Move>(Cash.Commands.Move::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Cash.Commands.Move {
|
||||
return Cash.Commands.Move(SecureHashGenerator().generate(random, status))
|
||||
}
|
||||
}
|
||||
|
||||
class IssueGenerator : Generator<Cash.Commands.Issue>(Cash.Commands.Issue::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Cash.Commands.Issue {
|
||||
return Cash.Commands.Issue(random.nextLong())
|
||||
}
|
||||
}
|
||||
|
||||
class ExitGenerator : Generator<Cash.Commands.Exit>(Cash.Commands.Exit::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Cash.Commands.Exit {
|
||||
return Cash.Commands.Exit(AmountGenerator(IssuedGenerator(CurrencyGenerator())).generate(random, status))
|
||||
}
|
||||
}
|
||||
|
||||
class CommandDataGenerator : Generator<CommandData>(CommandData::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): CommandData {
|
||||
val generators = listOf(MoveGenerator(), IssueGenerator(), ExitGenerator())
|
||||
return generators[random.nextInt(0, generators.size - 1)].generate(random, status)
|
||||
}
|
||||
}
|
||||
|
||||
class CommandGenerator : Generator<Command>(Command::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Command {
|
||||
val signersGenerator = ArrayListGenerator()
|
||||
signersGenerator.addComponentGenerators(listOf(PublicKeyGenerator()))
|
||||
return Command(CommandDataGenerator().generate(random, status), PublicKeyGenerator().generate(random, status))
|
||||
}
|
||||
}
|
||||
|
||||
class WiredTransactionGenerator: Generator<WireTransaction>(WireTransaction::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): WireTransaction {
|
||||
val commands = CommandGenerator().generateList(random, status) + listOf(CommandGenerator().generate(random, status))
|
||||
return WireTransaction(
|
||||
inputs = StateRefGenerator().generateList(random, status),
|
||||
attachments = SecureHashGenerator().generateList(random, status),
|
||||
outputs = TransactionStateGenerator(ContractStateGenerator()).generateList(random, status),
|
||||
commands = commands,
|
||||
notary = PartyGenerator().generate(random, status),
|
||||
signers = commands.flatMap { it.signers },
|
||||
type = TransactionType.General(),
|
||||
timestamp = TimestampGenerator().generate(random, status)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SignedTransactionGenerator: Generator<SignedTransaction>(SignedTransaction::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): SignedTransaction {
|
||||
val wireTransaction = WiredTransactionGenerator().generate(random, status)
|
||||
return SignedTransaction(
|
||||
txBits = wireTransaction.serialized,
|
||||
sigs = listOf(NullSignature)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.contracts.asset.sumCashBy
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.signWithECDSA
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.ServiceType
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import com.r3corda.core.utilities.ProgressTracker
|
||||
import com.r3corda.core.utilities.trace
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This asset trading protocol implements a "delivery vs payment" type swap. It has two parties (B and S for buyer
|
||||
* and seller) and the following steps:
|
||||
*
|
||||
* 1. S sends the [StateAndRef] pointing to what they want to sell to B, along with info about the price they require
|
||||
* B to pay. For example this has probably been agreed on an exchange.
|
||||
* 2. B sends to S a [SignedTransaction] that includes the state as input, B's cash as input, the state with the new
|
||||
* owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because
|
||||
* it lacks a signature from S authorising movement of the asset.
|
||||
* 3. S signs it and hands the now finalised SignedWireTransaction back to B.
|
||||
*
|
||||
* Assuming no malicious termination, they both end the protocol being in posession of a valid, signed transaction
|
||||
* that represents an atomic asset swap.
|
||||
*
|
||||
* Note that it's the *seller* who initiates contact with the buyer, not vice-versa as you might imagine.
|
||||
*
|
||||
* To initiate the protocol, use either the [runBuyer] or [runSeller] methods, depending on which side of the trade
|
||||
* your node is taking. These methods return a future which will complete once the trade is over and a fully signed
|
||||
* transaction is available: you can either block your thread waiting for the protocol to complete by using
|
||||
* [ListenableFuture.get] or more usefully, register a callback that will be invoked when the time comes.
|
||||
*
|
||||
* To see an example of how to use this class, look at the unit tests.
|
||||
*/
|
||||
// TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this
|
||||
// and [AbstractStateReplacementProtocol].
|
||||
object TwoPartyTradeProtocol {
|
||||
|
||||
class UnacceptablePriceException(val givenPrice: Amount<Currency>) : Exception("Unacceptable price: $givenPrice")
|
||||
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : Exception() {
|
||||
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
|
||||
}
|
||||
|
||||
// This object is serialised to the network and is the first protocol message the seller sends to the buyer.
|
||||
data class SellerTradeInfo(
|
||||
val assetForSale: StateAndRef<OwnableState>,
|
||||
val price: Amount<Currency>,
|
||||
val sellerOwnerKey: PublicKey
|
||||
)
|
||||
|
||||
data class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey,
|
||||
val notarySig: DigitalSignature.LegallyIdentifiable)
|
||||
|
||||
open class Seller(val otherParty: Party,
|
||||
val notaryNode: NodeInfo,
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount<Currency>,
|
||||
val myKeyPair: KeyPair,
|
||||
override val progressTracker: ProgressTracker = Seller.tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal")
|
||||
|
||||
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal")
|
||||
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
|
||||
object NOTARY : ProgressTracker.Step("Getting notary signature")
|
||||
|
||||
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to buyer")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val partialTX: SignedTransaction = receiveAndCheckProposedTransaction()
|
||||
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = calculateOurSignature(partialTX)
|
||||
val allPartySignedTx = partialTX + ourSignature
|
||||
val notarySignature = getNotarySignature(allPartySignedTx)
|
||||
return sendSignatures(allPartySignedTx, ourSignature, notarySignature)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = NOTARY
|
||||
return subProtocol(NotaryProtocol.Client(stx))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndCheckProposedTransaction(): SignedTransaction {
|
||||
progressTracker.currentStep = AWAITING_PROPOSAL
|
||||
|
||||
// Make the first message we'll send to kick off the protocol.
|
||||
val hello = SellerTradeInfo(assetToSell, price, myKeyPair.public)
|
||||
|
||||
val maybeSTX = sendAndReceive<SignedTransaction>(otherParty, hello)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
maybeSTX.unwrap {
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val wtx: WireTransaction = it.verifySignatures(myKeyPair.public, notaryNode.notaryIdentity.owningKey)
|
||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||
|
||||
// Download and check all the things that this transaction depends on and verify it is contract-valid,
|
||||
// even though it is missing signatures.
|
||||
subProtocol(ResolveTransactionsProtocol(wtx, otherParty))
|
||||
|
||||
if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public).withoutIssuer() != price)
|
||||
throw IllegalArgumentException("Transaction is not sending us the right amount of cash")
|
||||
|
||||
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
|
||||
//
|
||||
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
|
||||
// we're reusing keys! So don't reuse keys!
|
||||
// - This tx may include output states that impose odd conditions on the movement of the cash,
|
||||
// once we implement state pairing.
|
||||
//
|
||||
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
open fun calculateOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
progressTracker.currentStep = SIGNING
|
||||
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun sendSignatures(allPartySignedTx: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
notarySignature: DigitalSignature.LegallyIdentifiable): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_SIGS
|
||||
val fullySigned = allPartySignedTx + notarySignature
|
||||
|
||||
logger.trace { "Built finished transaction, sending back to secondary!" }
|
||||
|
||||
send(otherParty, SignaturesFromSeller(ourSignature, notarySignature))
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
|
||||
open class Buyer(val otherParty: Party,
|
||||
val notary: Party,
|
||||
val acceptablePrice: Amount<Currency>,
|
||||
val typeToBuy: Class<out OwnableState>) : ProtocolLogic<SignedTransaction>() {
|
||||
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for seller trading info")
|
||||
|
||||
object VERIFYING : ProgressTracker.Step("Verifying seller assets")
|
||||
|
||||
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
|
||||
|
||||
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the seller")
|
||||
|
||||
override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val tradeRequest = receiveAndValidateTradeRequest()
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest)
|
||||
val stx = signWithOurKeys(cashSigningPubKeys, ptx)
|
||||
|
||||
val signatures = swapSignaturesWithSeller(stx)
|
||||
|
||||
logger.trace { "Got signatures from seller, verifying ... " }
|
||||
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
||||
fullySigned.verifySignatures()
|
||||
|
||||
logger.trace { "Signatures received are valid. Trade complete! :-)" }
|
||||
return fullySigned
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveAndValidateTradeRequest(): SellerTradeInfo {
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Wait for a trade request to come in from the other side
|
||||
val maybeTradeRequest = receive<SellerTradeInfo>(otherParty)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
maybeTradeRequest.unwrap {
|
||||
// What is the seller trying to sell us?
|
||||
val asset = it.assetForSale.state.data
|
||||
val assetTypeName = asset.javaClass.name
|
||||
logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" }
|
||||
|
||||
if (it.price > acceptablePrice)
|
||||
throw UnacceptablePriceException(it.price)
|
||||
if (!typeToBuy.isInstance(asset))
|
||||
throw AssetMismatchException(typeToBuy.name, assetTypeName)
|
||||
|
||||
// Check the transaction that contains the state which is being resolved.
|
||||
// We only have a hash here, so if we don't know it already, we have to ask for it.
|
||||
subProtocol(ResolveTransactionsProtocol(setOf(it.assetForSale.ref.txhash), otherParty))
|
||||
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun swapSignaturesWithSeller(stx: SignedTransaction): SignaturesFromSeller {
|
||||
progressTracker.currentStep = SWAPPING_SIGNATURES
|
||||
logger.trace { "Sending partially signed transaction to seller" }
|
||||
|
||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||
|
||||
return sendAndReceive<SignaturesFromSeller>(otherParty, stx).unwrap { it }
|
||||
}
|
||||
|
||||
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (k in cashSigningPubKeys) {
|
||||
val priv = serviceHub.keyManagementService.toPrivate(k)
|
||||
ptx.signWith(KeyPair(k, priv))
|
||||
}
|
||||
|
||||
return ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
}
|
||||
|
||||
private fun assembleSharedTX(tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val ptx = TransactionType.General.Builder(notary)
|
||||
|
||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
|
||||
val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey)
|
||||
|
||||
// Add inputs/outputs/a command for the movement of the asset.
|
||||
tx.addInputState(tradeRequest.assetForSale)
|
||||
|
||||
// Just pick some new public key for now. This won't be linked with our identity in any way, which is what
|
||||
// we want for privacy reasons: the key is here ONLY to manage and control ownership, it is not intended to
|
||||
// reveal who the owner actually is. The key management service is expected to derive a unique key from some
|
||||
// initial seed in order to provide privacy protection.
|
||||
val freshKey = serviceHub.keyManagementService.freshKey()
|
||||
val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public)
|
||||
tx.addOutputState(state, tradeRequest.assetForSale.state.notary)
|
||||
tx.addCommand(command, tradeRequest.assetForSale.state.data.owner)
|
||||
|
||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||
// to have one.
|
||||
val currentTime = serviceHub.clock.instant()
|
||||
tx.setTime(currentTime, 30.seconds)
|
||||
return Pair(tx, cashSigningPubKeys)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: QuasarPlugin
|
||||
// Applying the maven plugin means this will get installed locally when running "gradle install"
|
||||
apply plugin: DefaultPublishTasks
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
@ -44,6 +43,7 @@ dependencies {
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
// Thread safety annotations
|
||||
@ -85,6 +85,9 @@ dependencies {
|
||||
|
||||
// JPA 2.1 annotations.
|
||||
compile "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final"
|
||||
|
||||
// RS API: Response type and codes for ApiUtils.
|
||||
compile "javax.ws.rs:javax.ws.rs-api:2.0"
|
||||
}
|
||||
|
||||
publishing {
|
||||
@ -97,6 +100,4 @@ publishing {
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quasarScan.dependsOn('classes')
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.core.crypto;
|
||||
package net.corda.core.crypto;
|
||||
|
||||
public class AddressFormatException extends IllegalArgumentException {
|
||||
public AddressFormatException() {
|
@ -1,4 +1,4 @@
|
||||
package com.r3corda.core.crypto;
|
||||
package net.corda.core.crypto;
|
||||
|
||||
import java.math.*;
|
||||
import java.util.*;
|
||||
@ -128,6 +128,7 @@ public class Base58 {
|
||||
* removed from the returned data.
|
||||
*
|
||||
* @param input the base58-encoded string to decode (which should include the checksum)
|
||||
* @return the original data bytes less the last 4 bytes (the checksum).
|
||||
* @throws AddressFormatException if the input is not base 58 or the checksum does not validate.
|
||||
*/
|
||||
public static byte[] decodeChecked(String input) throws AddressFormatException {
|
||||
@ -136,7 +137,7 @@ public class Base58 {
|
||||
throw new AddressFormatException("Input too short");
|
||||
byte[] data = Arrays.copyOfRange(decoded, 0, decoded.length - 4);
|
||||
byte[] checksum = Arrays.copyOfRange(decoded, decoded.length - 4, decoded.length);
|
||||
byte[] actualChecksum = Arrays.copyOfRange(SecureHash.sha256Twice(data).getBits(), 0, 4);
|
||||
byte[] actualChecksum = Arrays.copyOfRange(SecureHash.sha256Twice(data).getBytes(), 0, 4);
|
||||
if (!Arrays.equals(checksum, actualChecksum))
|
||||
throw new AddressFormatException("Checksum does not validate");
|
||||
return data;
|
@ -1,298 +0,0 @@
|
||||
package com.r3corda.core
|
||||
|
||||
import com.google.common.base.Function
|
||||
import com.google.common.base.Throwables
|
||||
import com.google.common.io.ByteStreams
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.r3corda.core.crypto.newSecureRandom
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.math.BigDecimal
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
import java.time.temporal.Temporal
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
val Int.days: Duration get() = Duration.ofDays(this.toLong())
|
||||
@Suppress("unused") // It's here for completeness
|
||||
val Int.hours: Duration get() = Duration.ofHours(this.toLong())
|
||||
val Int.minutes: Duration get() = Duration.ofMinutes(this.toLong())
|
||||
val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
|
||||
|
||||
|
||||
// TODO: Review by EOY2016 if we ever found these utilities helpful.
|
||||
val Int.bd: BigDecimal get() = BigDecimal(this)
|
||||
val Double.bd: BigDecimal get() = BigDecimal(this)
|
||||
val String.bd: BigDecimal get() = BigDecimal(this)
|
||||
val Long.bd: BigDecimal get() = BigDecimal(this)
|
||||
|
||||
fun String.abbreviate(maxWidth: Int): String = if (length <= maxWidth) this else take(maxWidth - 1) + "…"
|
||||
|
||||
/** Like the + operator but throws an exception in case of integer overflow. */
|
||||
infix fun Int.checkedAdd(b: Int) = Math.addExact(this, b)
|
||||
/** Like the + operator but throws an exception in case of integer overflow. */
|
||||
@Suppress("unused")
|
||||
infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b)
|
||||
|
||||
/**
|
||||
* Returns a random positive long generated using a secure RNG. This function sacrifies a bit of entropy in order to
|
||||
* avoid potential bugs where the value is used in a context where negative numbers are not expected.
|
||||
*/
|
||||
fun random63BitValue(): Long = Math.abs(newSecureRandom().nextLong())
|
||||
|
||||
// Some utilities for working with Guava listenable futures.
|
||||
fun <T> ListenableFuture<T>.then(executor: Executor, body: () -> Unit) = addListener(Runnable(body), executor)
|
||||
|
||||
fun <T> ListenableFuture<T>.success(executor: Executor, body: (T) -> Unit) = then(executor) {
|
||||
val r = try {
|
||||
get()
|
||||
} catch(e: Throwable) {
|
||||
return@then
|
||||
}
|
||||
body(r)
|
||||
}
|
||||
|
||||
fun <T> ListenableFuture<T>.failure(executor: Executor, body: (Throwable) -> Unit) = then(executor) {
|
||||
try {
|
||||
get()
|
||||
} catch (e: ExecutionException) {
|
||||
body(e.cause!!)
|
||||
} catch (t: Throwable) {
|
||||
body(t)
|
||||
}
|
||||
}
|
||||
|
||||
infix fun <T> ListenableFuture<T>.then(body: () -> Unit): ListenableFuture<T> = apply { then(RunOnCallerThread, body) }
|
||||
infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T> = apply { success(RunOnCallerThread, body) }
|
||||
infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) }
|
||||
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, Function { mapper(it!!) })
|
||||
infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) }
|
||||
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */
|
||||
inline fun <T> SettableFuture<T>.catch(block: () -> T) {
|
||||
try {
|
||||
set(block())
|
||||
} catch (t: Throwable) {
|
||||
setException(t)
|
||||
}
|
||||
}
|
||||
|
||||
fun <R> Path.use(block: (InputStream) -> R): R = Files.newInputStream(this).use(block)
|
||||
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
|
||||
|
||||
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
|
||||
infix fun Temporal.until(endExclusive: Temporal) = Duration.between(this, endExclusive)
|
||||
|
||||
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
|
||||
fun <T> List<T>.indexOfOrThrow(item: T): Int {
|
||||
val i = indexOf(item)
|
||||
require(i != -1)
|
||||
return i
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single element matching the given [predicate], or `null` if element was not found,
|
||||
* or throws if more than one element was found.
|
||||
*/
|
||||
fun <T> Iterable<T>.noneOrSingle(predicate: (T) -> Boolean): T? {
|
||||
var single: T? = null
|
||||
for (element in this) {
|
||||
if (predicate(element)) {
|
||||
if (single == null) {
|
||||
single = element
|
||||
} else throw IllegalArgumentException("Collection contains more than one matching element.")
|
||||
}
|
||||
}
|
||||
return single
|
||||
}
|
||||
|
||||
/** Returns single element, or `null` if element was not found, or throws if more than one element was found. */
|
||||
fun <T> Iterable<T>.noneOrSingle(): T? {
|
||||
var single: T? = null
|
||||
for (element in this) {
|
||||
if (single == null) {
|
||||
single = element
|
||||
} else throw IllegalArgumentException("Collection contains more than one matching element.")
|
||||
}
|
||||
return single
|
||||
}
|
||||
|
||||
// An alias that can sometimes make code clearer to read.
|
||||
val RunOnCallerThread = MoreExecutors.directExecutor()
|
||||
|
||||
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
|
||||
// returns in the IRSSimulationTest. If not, commit the inline back.
|
||||
fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T {
|
||||
val now = System.currentTimeMillis()
|
||||
val r = body()
|
||||
val elapsed = System.currentTimeMillis() - now
|
||||
if (logger != null)
|
||||
logger.info("$label took $elapsed msec")
|
||||
else
|
||||
println("$label took $elapsed msec")
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* A threadbox is a simple utility that makes it harder to forget to take a lock before accessing some shared state.
|
||||
* Simply define a private class to hold the data that must be grouped under the same lock, and then pass the only
|
||||
* instance to the ThreadBox constructor. You can now use the [locked] method with a lambda to take the lock in a
|
||||
* way that ensures it'll be released if there's an exception.
|
||||
*
|
||||
* Note that this technique is not infallible: if you capture a reference to the fields in another lambda which then
|
||||
* gets stored and invoked later, there may still be unsafe multi-threaded access going on, so watch out for that.
|
||||
* This is just a simple guard rail that makes it harder to slip up.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* private class MutableState { var i = 5 }
|
||||
* private val state = ThreadBox(MutableState())
|
||||
*
|
||||
* val ii = state.locked { i }
|
||||
*/
|
||||
class ThreadBox<out T>(val content: T, val lock: ReentrantLock = ReentrantLock()) {
|
||||
inline fun <R> locked(body: T.() -> R): R = lock.withLock { body(content) }
|
||||
inline fun <R> alreadyLocked(body: T.() -> R): R {
|
||||
check(lock.isHeldByCurrentThread, { "Expected $lock to already be locked." })
|
||||
return body(content)
|
||||
}
|
||||
fun checkNotLocked() = check(!lock.isHeldByCurrentThread)
|
||||
}
|
||||
|
||||
/**
|
||||
* This represents a transient exception or condition that might no longer be thrown if the operation is re-run or called
|
||||
* again.
|
||||
*
|
||||
* We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization.
|
||||
*/
|
||||
abstract class RetryableException(message: String) : Exception(message)
|
||||
|
||||
/**
|
||||
* A simple wrapper that enables the use of Kotlin's "val x by TransientProperty { ... }" syntax. Such a property
|
||||
* will not be serialized to disk, and if it's missing (or the first time it's accessed), the initializer will be
|
||||
* used to set it up. Note that the initializer will be called with the TransientProperty object locked.
|
||||
*/
|
||||
class TransientProperty<out T>(private val initializer: () -> T) {
|
||||
@Transient private var v: T? = null
|
||||
|
||||
@Synchronized
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
if (v == null)
|
||||
v = initializer()
|
||||
return v!!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a path to a zip file, extracts it to the given directory.
|
||||
*/
|
||||
fun extractZipFile(zipPath: Path, toPath: Path) {
|
||||
val normalisedToPath = toPath.normalize()
|
||||
if (!Files.exists(normalisedToPath))
|
||||
Files.createDirectories(normalisedToPath)
|
||||
|
||||
ZipInputStream(BufferedInputStream(Files.newInputStream(zipPath))).use { zip ->
|
||||
while (true) {
|
||||
val e = zip.nextEntry ?: break
|
||||
val outPath = normalisedToPath.resolve(e.name)
|
||||
|
||||
// Security checks: we should reject a zip that contains tricksy paths that try to escape toPath.
|
||||
if (!outPath.normalize().startsWith(normalisedToPath))
|
||||
throw IllegalStateException("ZIP contained a path that resolved incorrectly: ${e.name}")
|
||||
|
||||
if (e.isDirectory) {
|
||||
Files.createDirectories(outPath)
|
||||
continue
|
||||
}
|
||||
Files.newOutputStream(outPath).use { out ->
|
||||
ByteStreams.copy(zip, out)
|
||||
}
|
||||
zip.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Generic csv printing utility for clases.
|
||||
|
||||
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
|
||||
|
||||
/** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */
|
||||
operator fun Path.div(other: String): Path = resolve(other)
|
||||
|
||||
/** Representation of an operation that may have thrown an error. */
|
||||
data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) {
|
||||
constructor(value: A) : this(value, null)
|
||||
|
||||
companion object {
|
||||
/** Runs the given lambda and wraps the result. */
|
||||
inline fun <T> catch(body: () -> T): ErrorOr<T> = try { ErrorOr(body()) } catch (t: Throwable) { ErrorOr.of(t) }
|
||||
fun of(t: Throwable) = ErrorOr(null, t)
|
||||
}
|
||||
|
||||
fun <T> match(onValue: (A) -> T, onError: (Throwable) -> T): T {
|
||||
if (value != null) {
|
||||
return onValue(value)
|
||||
} else {
|
||||
return onError(error!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrThrow(): A {
|
||||
if (value != null) {
|
||||
return value
|
||||
} else {
|
||||
throw error!!
|
||||
}
|
||||
}
|
||||
|
||||
// Functor
|
||||
fun <B> map(function: (A) -> B) = ErrorOr(value?.let(function), error)
|
||||
|
||||
// Applicative
|
||||
fun <B, C> combine(other: ErrorOr<B>, function: (A, B) -> C): ErrorOr<C> {
|
||||
return ErrorOr(value?.let { a -> other.value?.let { b -> function(a, b) } }, error ?: other.error)
|
||||
}
|
||||
|
||||
// Monad
|
||||
fun <B> bind(function: (A) -> ErrorOr<B>) = value?.let(function) ?: ErrorOr.of(error!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that buffers events until subscribed.
|
||||
*
|
||||
* @see UnicastSubject
|
||||
*/
|
||||
fun <T> Observable<T>.bufferUntilSubscribed(): Observable<T> {
|
||||
val subject = UnicastSubject.create<T>()
|
||||
val subscription = subscribe(subject)
|
||||
return subject.doOnUnsubscribe { subscription.unsubscribe() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an iterable data type's contents are ordered and unique, based on their [Comparable].compareTo
|
||||
* function.
|
||||
*/
|
||||
fun <T, I: Comparable<I>> Iterable<T>.isOrderedAndUnique(extractId: T.() -> I): Boolean {
|
||||
var last: I? = null
|
||||
return all { it ->
|
||||
val lastLast = last
|
||||
last = extractId(it)
|
||||
if (lastLast == null) {
|
||||
true
|
||||
} else {
|
||||
lastLast < extractId(it)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package com.r3corda.core.contracts
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A command from the monitoring client, to the node.
|
||||
*
|
||||
* @param id ID used to tag event(s) resulting from a command.
|
||||
*/
|
||||
sealed class ClientToServiceCommand(val id: UUID) {
|
||||
/**
|
||||
* Issue cash state objects.
|
||||
*
|
||||
* @param amount the amount of currency to issue on to the ledger.
|
||||
* @param issueRef the reference to specify on the issuance, used to differentiate pools of cash. Convention is
|
||||
* to use the single byte "0x01" as a default.
|
||||
* @param recipient the party to issue the cash to.
|
||||
* @param notary the notary to use for this transaction.
|
||||
* @param id the ID to be provided in events resulting from this request.
|
||||
*/
|
||||
class IssueCash(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val notary: Party,
|
||||
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
|
||||
|
||||
/**
|
||||
* Pay cash to someone else.
|
||||
*
|
||||
* @param amount the amount of currency to issue on to the ledger.
|
||||
* @param recipient the party to issue the cash to.
|
||||
* @param id the ID to be provided in events resulting from this request.
|
||||
*/
|
||||
class PayCash(val amount: Amount<Issued<Currency>>, val recipient: Party,
|
||||
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
|
||||
|
||||
/**
|
||||
* Exit cash from the ledger.
|
||||
*
|
||||
* @param amount the amount of currency to exit from the ledger.
|
||||
* @param issueRef the reference previously specified on the issuance.
|
||||
* @param id the ID to be provided in events resulting from this request.
|
||||
*/
|
||||
class ExitCash(val amount: Amount<Currency>, val issueRef: OpaqueBytes,
|
||||
id: UUID = UUID.randomUUID()) : ClientToServiceCommand(id)
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package com.r3corda.core.contracts
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
|
||||
val DUMMY_PROGRAM_ID = DummyContract()
|
||||
|
||||
class DummyContract : Contract {
|
||||
|
||||
interface State : ContractState {
|
||||
val magicNumber: Int
|
||||
}
|
||||
|
||||
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative state with multiple owners. This exists primarily to provide a dummy state with multiple
|
||||
* participants, and could in theory be merged with [SingleOwnerState] by putting the additional participants
|
||||
* in a different field, however this is a good example of a contract with multiple states.
|
||||
*/
|
||||
data class MultiOwnerState(override val magicNumber: Int = 0,
|
||||
val owners: List<PublicKey>) : ContractState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
get() = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Create : TypeOnlyCommandData(), Commands
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
|
||||
override fun verify(tx: TransactionForContract) {
|
||||
// Always accepts.
|
||||
}
|
||||
|
||||
// The "empty contract"
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("")
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun generateInitial(owner: PartyAndReference, magicNumber: Int, notary: Party): TransactionBuilder {
|
||||
val state = SingleOwnerState(magicNumber, owner.party.owningKey)
|
||||
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey))
|
||||
}
|
||||
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: PublicKey): TransactionBuilder {
|
||||
require(priors.size > 0)
|
||||
val priorState = priors[0].state.data
|
||||
val (cmd, state) = priorState.withNewOwner(newOwner)
|
||||
return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
|
||||
/* INPUTS */ *priors.toTypedArray(),
|
||||
/* COMMAND */ Command(cmd, priorState.owner),
|
||||
/* OUTPUT */ state
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package com.r3corda.core.contracts
|
||||
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
|
||||
*/
|
||||
data class DummyState(val magicNumber: Int = 0) : ContractState {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
get() = emptyList()
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package com.r3corda.core.contracts
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
/** Defines transaction build & validation logic for a specific transaction type */
|
||||
sealed class TransactionType {
|
||||
override fun equals(other: Any?) = other?.javaClass == javaClass
|
||||
override fun hashCode() = javaClass.name.hashCode()
|
||||
|
||||
/**
|
||||
* Check that the transaction is valid based on:
|
||||
* - General platform rules
|
||||
* - Rules for the specific transaction type
|
||||
*
|
||||
* Note: Presence of _signatures_ is not checked, only the public keys to be signed for.
|
||||
*/
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
|
||||
val missing = verifySigners(tx)
|
||||
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
|
||||
verifyTransaction(tx)
|
||||
}
|
||||
|
||||
/** Check that the list of signers includes all the necessary keys */
|
||||
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
|
||||
val notaryKey = tx.inputs.map { it.state.notary.owningKey }.toSet()
|
||||
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
|
||||
|
||||
val requiredKeys = getRequiredSigners(tx) + notaryKey
|
||||
val missing = requiredKeys - tx.mustSign
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of public keys that that require signatures for the transaction type.
|
||||
* Note: the notary key is checked separately for all transactions and need not be included.
|
||||
*/
|
||||
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<PublicKey>
|
||||
|
||||
/** Implement type specific transaction validation logic */
|
||||
abstract fun verifyTransaction(tx: LedgerTransaction)
|
||||
|
||||
/** A general transaction type where transaction validity is determined by custom contract code */
|
||||
class General : TransactionType() {
|
||||
/** Just uses the default [TransactionBuilder] with no special logic */
|
||||
class Builder(notary: Party?) : TransactionBuilder(General(), notary) {}
|
||||
|
||||
/**
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
override fun verifyTransaction(tx: LedgerTransaction) {
|
||||
// Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
|
||||
// are any inputs, all outputs must have the same notary.
|
||||
// TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
|
||||
// flexible on output notaries.
|
||||
if (tx.notary != null
|
||||
&& tx.inputs.isNotEmpty()) {
|
||||
tx.outputs.forEach {
|
||||
if (it.notary != tx.notary) {
|
||||
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx, it.notary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ctx = tx.toTransactionForContract()
|
||||
|
||||
// TODO: This will all be replaced in future once the sandbox and contract constraints work is done.
|
||||
val contracts = (ctx.inputs.map { it.contract } + ctx.outputs.map { it.contract }).toSet()
|
||||
for (contract in contracts) {
|
||||
try {
|
||||
contract.verify(ctx)
|
||||
} catch(e: Throwable) {
|
||||
throw TransactionVerificationException.ContractRejection(tx, contract, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that all encumbrances exist within the set of input states.
|
||||
tx.inputs.filter { it.state.data.encumbrance != null }.forEach {
|
||||
encumberedInput ->
|
||||
if (tx.inputs.none { it.ref.txhash == encumberedInput.ref.txhash &&
|
||||
it.ref.index == encumberedInput.state.data.encumbrance }) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumberedInput.state.data.encumbrance!!,
|
||||
TransactionVerificationException.Direction.INPUT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance,
|
||||
// and that the number of outputs can contain the encumbrance.
|
||||
for ((i, output) in tx.outputs.withIndex() ) {
|
||||
val encumbranceIndex = output.data.encumbrance ?: continue
|
||||
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
|
||||
throw TransactionVerificationException.TransactionMissingEncumbranceException(
|
||||
tx, encumbranceIndex,
|
||||
TransactionVerificationException.Direction.OUTPUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* A special transaction type for reassigning a notary for a state. Validation does not involve running
|
||||
* any contract code, it just checks that the states are unmodified apart from the notary field.
|
||||
*/
|
||||
class NotaryChange : TransactionType() {
|
||||
/**
|
||||
* A transaction builder that automatically sets the transaction type to [NotaryChange]
|
||||
* and adds the list of participants to the signers set for every input state.
|
||||
*/
|
||||
class Builder(notary: Party) : TransactionBuilder(NotaryChange(), notary) {
|
||||
override fun addInputState(stateAndRef: StateAndRef<*>) {
|
||||
signers.addAll(stateAndRef.state.data.participants)
|
||||
super.addInputState(stateAndRef)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the difference between inputs and outputs is only the notary field, and that all required signing
|
||||
* public keys are present.
|
||||
*
|
||||
* @throws TransactionVerificationException.InvalidNotaryChange if the validity check fails.
|
||||
*/
|
||||
override fun verifyTransaction(tx: LedgerTransaction) {
|
||||
try {
|
||||
for ((input, output) in tx.inputs.zip(tx.outputs)) {
|
||||
check(input.state.data == output.data)
|
||||
check(input.state.notary != output.notary)
|
||||
}
|
||||
check(tx.commands.isEmpty())
|
||||
} catch (e: IllegalStateException) {
|
||||
throw TransactionVerificationException.InvalidNotaryChange(tx)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet()
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
@file:JvmName("ClauseVerifier")
|
||||
package com.r3corda.core.contracts.clauses
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
|
||||
/**
|
||||
* Verify a transaction against the given list of clauses.
|
||||
*
|
||||
* @param tx transaction to be verified.
|
||||
* @param clauses the clauses to verify.
|
||||
* @param commands commands extracted from the transaction, which are relevant to the
|
||||
* clauses.
|
||||
*/
|
||||
fun <C: CommandData> verifyClause(tx: TransactionForContract,
|
||||
clause: Clause<ContractState, C, Unit>,
|
||||
commands: List<AuthenticatedObject<C>>) {
|
||||
if (Clause.log.isTraceEnabled) {
|
||||
clause.getExecutionPath(commands).forEach {
|
||||
Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
|
||||
}
|
||||
}
|
||||
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)
|
||||
|
||||
check(matchedCommands.containsAll(commands.map { it.value })) { "The following commands were not matched at the end of execution: " + (commands - matchedCommands) }
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package com.r3corda.core.contracts.clauses
|
||||
|
||||
import com.r3corda.core.contracts.AuthenticatedObject
|
||||
import com.r3corda.core.contracts.CommandData
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
|
||||
/**
|
||||
* Abstract supertype for clauses which compose other clauses together in some logical manner.
|
||||
*/
|
||||
abstract class CompositeClause<in S : ContractState, C: CommandData, in K : Any>: Clause<S, C, K>() {
|
||||
/** List of clauses under this composite clause */
|
||||
abstract val clauses: List<Clause<S, C, K>>
|
||||
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= matchedClauses(commands).flatMap { it.getExecutionPath(commands) }
|
||||
/** Determine which clauses are matched by the supplied commands */
|
||||
abstract fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>>
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
package com.r3corda.core.crypto
|
||||
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.i2p.crypto.eddsa.KeyPairGenerator
|
||||
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
|
||||
fun newSecureRandom(): SecureRandom {
|
||||
if (System.getProperty("os.name") == "Linux") {
|
||||
return SecureRandom.getInstance("NativePRNGNonBlocking")
|
||||
} else {
|
||||
return SecureRandom.getInstanceStrong()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a digital signature. The covering field is a generic tag usable by whatever is interpreting the
|
||||
* signature. It isn't used currently, but experience from Bitcoin suggests such a feature is useful, especially when
|
||||
* building partially signed transactions.
|
||||
*/
|
||||
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
|
||||
/** A digital signature that identifies who the public key is owned by. */
|
||||
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
|
||||
fun verifyWithECDSA(content: ByteArray) = by.verifyWithECDSA(content, this)
|
||||
fun verifyWithECDSA(content: OpaqueBytes) = by.verifyWithECDSA(content.bits, this)
|
||||
}
|
||||
|
||||
class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey, bits)
|
||||
}
|
||||
|
||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "NULL"
|
||||
override fun getEncoded() = byteArrayOf(0)
|
||||
override fun getFormat() = "NULL"
|
||||
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
|
||||
override fun toString() = "NULL_KEY"
|
||||
}
|
||||
|
||||
// TODO: Clean up this duplication between Null and Dummy public key
|
||||
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "DUMMY"
|
||||
override fun getEncoded() = s.toByteArray()
|
||||
override fun getFormat() = "ASN.1"
|
||||
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
|
||||
override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
|
||||
override fun hashCode(): Int = s.hashCode()
|
||||
override fun toString() = "PUBKEY[$s]"
|
||||
}
|
||||
|
||||
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
|
||||
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
|
||||
|
||||
/** Utility to simplify the act of signing a byte array */
|
||||
fun PrivateKey.signWithECDSA(bits: ByteArray): DigitalSignature {
|
||||
val signer = EdDSAEngine()
|
||||
signer.initSign(this)
|
||||
signer.update(bits)
|
||||
val sig = signer.sign()
|
||||
return DigitalSignature(sig)
|
||||
}
|
||||
|
||||
fun PrivateKey.signWithECDSA(bitsToSign: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey {
|
||||
return DigitalSignature.WithKey(publicKey, signWithECDSA(bitsToSign).bits)
|
||||
}
|
||||
|
||||
val ed25519Curve = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)
|
||||
|
||||
fun parsePublicKeyBase58(base58String: String) = EdDSAPublicKey(EdDSAPublicKeySpec(Base58.decode(base58String), ed25519Curve))
|
||||
fun PublicKey.toBase58String() = Base58.encode((this as EdDSAPublicKey).abyte)
|
||||
|
||||
fun KeyPair.signWithECDSA(bitsToSign: ByteArray) = private.signWithECDSA(bitsToSign, public)
|
||||
fun KeyPair.signWithECDSA(bitsToSign: OpaqueBytes) = private.signWithECDSA(bitsToSign.bits, public)
|
||||
fun KeyPair.signWithECDSA(bitsToSign: OpaqueBytes, party: Party) = signWithECDSA(bitsToSign.bits, party)
|
||||
fun KeyPair.signWithECDSA(bitsToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
|
||||
check(public == party.owningKey)
|
||||
val sig = signWithECDSA(bitsToSign)
|
||||
return DigitalSignature.LegallyIdentifiable(party, sig.bits)
|
||||
}
|
||||
|
||||
/** Utility to simplify the act of verifying a signature */
|
||||
fun PublicKey.verifyWithECDSA(content: ByteArray, signature: DigitalSignature) {
|
||||
val verifier = EdDSAEngine()
|
||||
verifier.initVerify(this)
|
||||
verifier.update(content)
|
||||
if (verifier.verify(signature.bits) == false)
|
||||
throw SignatureException("Signature did not match")
|
||||
}
|
||||
|
||||
/** Render a public key to a string, using a short form if it's an elliptic curve public key */
|
||||
fun PublicKey.toStringShort(): String {
|
||||
return (this as? EdDSAPublicKey)?.let { key ->
|
||||
"DL" + Base58.encode(key.abyte) // DL -> Distributed Ledger
|
||||
} ?: toString()
|
||||
}
|
||||
|
||||
fun Iterable<PublicKey>.toStringsShort(): String = map { it.toStringShort() }.toString()
|
||||
|
||||
/** Creates a [PublicKeyTree] with a single leaf node containing the public key */
|
||||
val PublicKey.tree: PublicKeyTree get() = PublicKeyTree.Leaf(this)
|
||||
|
||||
/** Returns the set of all [PublicKey]s of the signatures */
|
||||
fun Iterable<DigitalSignature.WithKey>.byKeys() = map { it.by }.toSet()
|
||||
|
||||
// Allow Kotlin destructuring: val (private, public) = keyPair
|
||||
operator fun KeyPair.component1() = this.private
|
||||
|
||||
operator fun KeyPair.component2() = this.public
|
||||
|
||||
/** A simple wrapper that will make it easier to swap out the EC algorithm we use in future */
|
||||
fun generateKeyPair(): KeyPair = KeyPairGenerator().generateKeyPair()
|
||||
|
||||
/**
|
||||
* Returns a key pair derived from the given private key entropy. This is useful for unit tests and other cases where
|
||||
* you want hard-coded private keys.
|
||||
*/
|
||||
fun entropyToKeyPair(entropy: BigInteger): KeyPair {
|
||||
val params = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512)
|
||||
val bits = entropy.toByteArray().copyOf(params.curve.field.getb() / 8)
|
||||
val priv = EdDSAPrivateKeySpec(bits, params)
|
||||
val pub = EdDSAPublicKeySpec(priv.a, params)
|
||||
val key = KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
|
||||
return key
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package com.r3corda.core.crypto
|
||||
|
||||
import com.r3corda.core.contracts.PartyAndReference
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
|
||||
/** A [Party] is well known (name, pubkey) pair. In a real system this would probably be an X.509 certificate. */
|
||||
data class Party(val name: String, val owningKey: PublicKey) {
|
||||
override fun toString() = name
|
||||
|
||||
fun ref(bytes: OpaqueBytes) = PartyAndReference(this, bytes)
|
||||
fun ref(vararg bytes: Byte) = ref(OpaqueBytes.of(*bytes))
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package com.r3corda.core.crypto
|
||||
|
||||
import com.r3corda.core.crypto.PublicKeyTree.Leaf
|
||||
import com.r3corda.core.crypto.PublicKeyTree.Node
|
||||
import com.r3corda.core.serialization.deserialize
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A tree data structure that enables the representation of composite public keys.
|
||||
*
|
||||
* In the simplest case it may just contain a single node encapsulating a [PublicKey] – a [Leaf].
|
||||
*
|
||||
* For more complex scenarios, such as *"Both Alice and Bob need to sign to consume a state S"*, we can represent
|
||||
* the requirement by creating a tree with a root [Node], and Alice and Bob as children – [Leaf]s.
|
||||
* The root node would specify *weights* for each of its children and a *threshold* – the minimum total weight required
|
||||
* (e.g. the minimum number of child signatures required) to satisfy the tree signature requirement.
|
||||
*
|
||||
* Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can
|
||||
* create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*.
|
||||
*/
|
||||
sealed class PublicKeyTree {
|
||||
/** Checks whether [keys] match a sufficient amount of leaf nodes */
|
||||
abstract fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean
|
||||
|
||||
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
|
||||
|
||||
/** Returns all [PublicKey]s contained within the tree leaves */
|
||||
abstract fun getKeys(): Set<PublicKey>
|
||||
|
||||
/** Checks whether any of the given [keys] matches a leaf on the tree */
|
||||
fun containsAny(keys: Iterable<PublicKey>) = getKeys().intersect(keys).isNotEmpty()
|
||||
|
||||
// TODO: implement a proper encoding/decoding mechanism
|
||||
fun toBase58String(): String = Base58.encode(this.serialize().bits)
|
||||
|
||||
companion object {
|
||||
fun parseFromBase58(encoded: String) = Base58.decode(encoded).deserialize<PublicKeyTree>()
|
||||
}
|
||||
|
||||
/** The leaf node of the public key tree – a wrapper around a [PublicKey] primitive */
|
||||
class Leaf(val publicKey: PublicKey) : PublicKeyTree() {
|
||||
override fun isFulfilledBy(keys: Iterable<PublicKey>) = publicKey in keys
|
||||
|
||||
override fun getKeys(): Set<PublicKey> = setOf(publicKey)
|
||||
|
||||
// Auto-generated. TODO: remove once data class inheritance is enabled
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as Leaf
|
||||
|
||||
if (publicKey != other.publicKey) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = publicKey.hashCode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a node in the [PublicKeyTree]. It maintains a list of child nodes – sub-trees, and associated
|
||||
* [weights] carried by child node signatures.
|
||||
*
|
||||
* The [threshold] specifies the minimum total weight required (in the simple case – the minimum number of child
|
||||
* signatures required) to satisfy the public key sub-tree rooted at this node.
|
||||
*/
|
||||
class Node(val threshold: Int,
|
||||
val children: List<PublicKeyTree>,
|
||||
val weights: List<Int>) : PublicKeyTree() {
|
||||
|
||||
override fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean {
|
||||
val totalWeight = children.mapIndexed { i, childNode ->
|
||||
if (childNode.isFulfilledBy(keys)) weights[i] else 0
|
||||
}.sum()
|
||||
|
||||
return totalWeight >= threshold
|
||||
}
|
||||
|
||||
override fun getKeys(): Set<PublicKey> = children.flatMap { it.getKeys() }.toSet()
|
||||
|
||||
// Auto-generated. TODO: remove once data class inheritance is enabled
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as Node
|
||||
|
||||
if (threshold != other.threshold) return false
|
||||
if (weights != other.weights) return false
|
||||
if (children != other.children) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = threshold
|
||||
result = 31 * result + weights.hashCode()
|
||||
result = 31 * result + children.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** A helper class for building a [PublicKeyTree.Node]. */
|
||||
class Builder() {
|
||||
private val children: MutableList<PublicKeyTree> = mutableListOf()
|
||||
private val weights: MutableList<Int> = mutableListOf()
|
||||
|
||||
/** Adds a child [PublicKeyTree] node. Specifying a [weight] for the child is optional and will default to 1. */
|
||||
fun addKey(publicKey: PublicKeyTree, weight: Int = 1): Builder {
|
||||
children.add(publicKey)
|
||||
weights.add(weight)
|
||||
return this
|
||||
}
|
||||
|
||||
fun addKeys(vararg publicKeys: PublicKeyTree): Builder {
|
||||
publicKeys.forEach { addKey(it) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun addLeaves(publicKeys: List<PublicKey>): Builder = addLeaves(*publicKeys.toTypedArray())
|
||||
fun addLeaves(vararg publicKeys: PublicKey) = addKeys(*publicKeys.map { it.tree }.toTypedArray())
|
||||
|
||||
/**
|
||||
* Builds the [PublicKeyTree.Node]. If [threshold] is not specified, it will default to
|
||||
* the size of the children, effectively generating an "N of N" requirement.
|
||||
*/
|
||||
fun build(threshold: Int? = null): PublicKeyTree {
|
||||
return if (children.size == 1) children.first()
|
||||
else Node(threshold ?: children.size, children.toList(), weights.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the set of all [PublicKey]s contained in the leaves of the [PublicKeyTree]s */
|
||||
fun Iterable<PublicKeyTree>.getKeys() = flatMap { it.getKeys() }.toSet()
|
@ -1,44 +0,0 @@
|
||||
package com.r3corda.core.crypto
|
||||
|
||||
import com.google.common.io.BaseEncoding
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Container for a cryptographically secure hash value.
|
||||
* Provides utilities for generating a cryptographic hash using different algorithms (currently only SHA-256 supported).
|
||||
*/
|
||||
sealed class SecureHash(bits: ByteArray) : OpaqueBytes(bits) {
|
||||
/** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes) */
|
||||
class SHA256(bits: ByteArray) : SecureHash(bits) {
|
||||
init {
|
||||
require(bits.size == 32)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = BaseEncoding.base16().encode(bits)
|
||||
|
||||
fun prefixChars(prefixLen: Int = 6) = toString().substring(0, prefixLen)
|
||||
|
||||
// Like static methods in Java, except the 'companion' is a singleton that can have state.
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun parse(str: String) = BaseEncoding.base16().decode(str.toUpperCase()).let {
|
||||
when (it.size) {
|
||||
32 -> SHA256(it)
|
||||
else -> throw IllegalArgumentException("Provided string is ${it.size} bytes not 32 bytes in hex: $str")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic fun sha256(bits: ByteArray) = SHA256(MessageDigest.getInstance("SHA-256").digest(bits))
|
||||
@JvmStatic fun sha256Twice(bits: ByteArray) = sha256(sha256(bits).bits)
|
||||
@JvmStatic fun sha256(str: String) = sha256(str.toByteArray())
|
||||
|
||||
@JvmStatic fun randomSHA256() = sha256(newSecureRandom().generateSeed(32))
|
||||
}
|
||||
|
||||
// In future, maybe SHA3, truncated hashes etc.
|
||||
}
|
||||
|
||||
fun ByteArray.sha256(): SecureHash.SHA256 = SecureHash.sha256(this)
|
||||
fun OpaqueBytes.sha256(): SecureHash.SHA256 = SecureHash.sha256(this.bits)
|
@ -1,38 +0,0 @@
|
||||
package com.r3corda.core.node
|
||||
|
||||
/**
|
||||
* Implement this interface on a class advertised in a META-INF/services/com.r3corda.core.node.CordaPluginRegistry file
|
||||
* to extend a Corda node with additional application services.
|
||||
*/
|
||||
abstract class CordaPluginRegistry {
|
||||
/**
|
||||
* List of JAX-RS classes inside the contract jar. They are expected to have a single parameter constructor that takes a ServiceHub as input.
|
||||
* These are listed as Class<*>, because in the future they will be instantiated inside a ClassLoader so that
|
||||
* Cordapp code can be loaded dynamically.
|
||||
*/
|
||||
open val webApis: List<Class<*>> = emptyList()
|
||||
|
||||
/**
|
||||
* Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*.
|
||||
* Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can
|
||||
* be specified with: javaClass.getResource("<folder-in-jar>").toExternalForm()
|
||||
*/
|
||||
open val staticServeDirs: Map<String, String> = emptyMap()
|
||||
|
||||
/**
|
||||
* A Map with an entry for each consumed protocol used by the webAPIs.
|
||||
* The key of each map entry should contain the ProtocolLogic<T> class name.
|
||||
* The associated map values are the union of all concrete class names passed to the protocol constructor.
|
||||
* Standard java.lang.* and kotlin.* types do not need to be included explicitly.
|
||||
* This is used to extend the white listed protocols that can be initiated from the ServiceHub invokeProtocolAsync method.
|
||||
*/
|
||||
open val requiredProtocols: Map<String, Set<String>> = emptyMap()
|
||||
|
||||
/**
|
||||
* List of additional long lived services to be hosted within the node.
|
||||
* They are expected to have a single parameter constructor that takes a ServiceHubInternal as input.
|
||||
* The ServiceHubInternal will be fully constructed before the plugin service is created and will
|
||||
* allow access to the protocol factory and protocol initiation entry points there.
|
||||
*/
|
||||
open val servicePlugins: List<Class<*>> = emptyList()
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package com.r3corda.core.node
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.node.services.ServiceType
|
||||
|
||||
/**
|
||||
* Information for an advertised service including the service specific identity information.
|
||||
* The identity can be used in protocols and is distinct from the Node's legalIdentity
|
||||
*/
|
||||
data class ServiceEntry(val info: ServiceInfo, val identity: Party)
|
||||
|
||||
/**
|
||||
* Info about a network node that acts on behalf of some form of contract party.
|
||||
*/
|
||||
data class NodeInfo(val address: SingleMessageRecipient,
|
||||
val legalIdentity: Party,
|
||||
var advertisedServices: List<ServiceEntry> = emptyList(),
|
||||
val physicalLocation: PhysicalLocation? = null) {
|
||||
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity
|
||||
fun serviceIdentities(type: ServiceType): List<Party> = advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package com.r3corda.core.node
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.contracts.StateRef
|
||||
import com.r3corda.core.contracts.TransactionResolutionException
|
||||
import com.r3corda.core.contracts.TransactionState
|
||||
import com.r3corda.core.messaging.MessagingService
|
||||
import com.r3corda.core.node.services.*
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import java.security.KeyPair
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* A service hub simply vends references to the other services a node has. Some of those services may be missing or
|
||||
* mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of
|
||||
* functionality and you don't want to hard-code which types in the interface.
|
||||
*
|
||||
* Any services exposed to protocols (public view) need to implement [SerializeAsToken] or similar to avoid their internal
|
||||
* state from being serialized in checkpoints.
|
||||
*/
|
||||
interface ServiceHub {
|
||||
val vaultService: VaultService
|
||||
val keyManagementService: KeyManagementService
|
||||
val identityService: IdentityService
|
||||
val storageService: StorageService
|
||||
val networkService: MessagingService
|
||||
val networkMapCache: NetworkMapCache
|
||||
val schedulerService: SchedulerService
|
||||
val clock: Clock
|
||||
val myInfo: NodeInfo
|
||||
|
||||
/**
|
||||
* Given a list of [SignedTransaction]s, writes them to the local storage for validated transactions and then
|
||||
* sends them to the vault for further processing.
|
||||
*
|
||||
* @param txs The transactions to record.
|
||||
*/
|
||||
fun recordTransactions(txs: Iterable<SignedTransaction>)
|
||||
|
||||
/**
|
||||
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
|
||||
*
|
||||
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction.
|
||||
*/
|
||||
fun loadState(stateRef: StateRef): TransactionState<*> {
|
||||
val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||
return definingTx.tx.outputs[stateRef.index]
|
||||
}
|
||||
|
||||
/**
|
||||
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the protocol.
|
||||
*
|
||||
* @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args].
|
||||
*/
|
||||
fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T>
|
||||
|
||||
/**
|
||||
* Helper property to shorten code for fetching the Node's KeyPair associated with the
|
||||
* public legalIdentity Party from the key management service.
|
||||
* Typical use is during signing in protocols and for unit test signing.
|
||||
*/
|
||||
val legalIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.legalIdentity.owningKey)
|
||||
|
||||
/**
|
||||
* Helper property to shorten code for fetching the Node's KeyPair associated with the
|
||||
* public notaryIdentity Party from the key management service. It is assumed that this is only
|
||||
* used in contexts where the Node knows it is hosting a Notary Service. Otherwise, it will throw
|
||||
* an IllegalArgumentException.
|
||||
* Typical use is during signing in protocols and for unit test signing.
|
||||
*/
|
||||
val notaryIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.notaryIdentity.owningKey)
|
||||
|
||||
}
|
||||
/**
|
||||
* Given some [SignedTransaction]s, writes them to the local storage for validated transactions and then
|
||||
* sends them to the vault for further processing.
|
||||
*
|
||||
* @param txs The transactions to record.
|
||||
*/
|
||||
fun ServiceHub.recordTransactions(vararg txs: SignedTransaction) = recordTransactions(txs.toList())
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user