mirror of
https://github.com/corda/corda.git
synced 2025-01-02 03:06:45 +00:00
Merge with master
# Conflicts: # docs/build/html/_sources/index.txt # docs/build/html/_sources/protocol-state-machines.txt # docs/build/html/_sources/tutorial-contract-clauses.txt # docs/build/html/index.html # docs/build/html/protocol-state-machines.html # docs/build/html/searchindex.js # docs/build/html/tutorial-contract-clauses.html # docs/build/html/tutorial-contract.html
This commit is contained in:
commit
b7e6c210d9
2
.gitignore
vendored
2
.gitignore
vendored
@ -16,6 +16,8 @@ tags
|
|||||||
/core/build
|
/core/build
|
||||||
/experimental/build
|
/experimental/build
|
||||||
/docs/build/doctrees
|
/docs/build/doctrees
|
||||||
|
/test-utils/build
|
||||||
|
/client/build
|
||||||
|
|
||||||
# gradle's buildSrc build/
|
# gradle's buildSrc build/
|
||||||
/buildSrc/build/
|
/buildSrc/build/
|
||||||
|
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@ -5,6 +5,10 @@
|
|||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc.iml" />
|
<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_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/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.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_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/contracts/contracts_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" group="contracts" />
|
||||||
@ -18,12 +22,16 @@
|
|||||||
<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_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/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/node/node.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node.iml" group="node" />
|
<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_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/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.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_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_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/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>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -42,7 +42,7 @@ Install the Oracle JDK 8u45 or higher. It is possible that OpenJDK will also wor
|
|||||||
|
|
||||||
## Using IntelliJ
|
## Using IntelliJ
|
||||||
|
|
||||||
It's a good idea to use a modern IDE. We use IntelliJ. Install IntelliJ version 15 community edition (which is free):
|
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/
|
https://www.jetbrains.com/idea/download/
|
||||||
|
|
||||||
|
39
build.gradle
39
build.gradle
@ -1,6 +1,6 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.0.3'
|
ext.kotlin_version = '1.0.3'
|
||||||
ext.quasar_version = '0.7.5'
|
ext.quasar_version = '0.7.6'
|
||||||
ext.asm_version = '0.5.3'
|
ext.asm_version = '0.5.3'
|
||||||
ext.artemis_version = '1.3.0'
|
ext.artemis_version = '1.3.0'
|
||||||
ext.jackson_version = '2.8.0.rc2'
|
ext.jackson_version = '2.8.0.rc2'
|
||||||
@ -48,7 +48,7 @@ allprojects {
|
|||||||
|
|
||||||
// Our version: bump this on release.
|
// Our version: bump this on release.
|
||||||
group 'com.r3corda'
|
group 'com.r3corda'
|
||||||
version '0.3-SNAPSHOT'
|
version '0.4-SNAPSHOT'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@ -102,21 +102,24 @@ mainClassName = 'com.r3corda.demos.TraderDemoKt'
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':node')
|
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-stdlib:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
||||||
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
||||||
|
compile 'co.paralleluniverse:capsule:1.0.3'
|
||||||
|
|
||||||
// Unit testing helpers.
|
// Unit testing helpers.
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
testCompile 'org.assertj:assertj-core:3.4.1'
|
testCompile 'org.assertj:assertj-core:3.4.1'
|
||||||
testCompile 'com.pholser:junit-quickcheck-core:0.6'
|
|
||||||
|
|
||||||
// Integration test helpers
|
// Integration test helpers
|
||||||
integrationTestCompile 'junit:junit:4.12'
|
integrationTestCompile 'junit:junit:4.12'
|
||||||
integrationTestCompile 'org.assertj:assertj-core:${assertj_version}'
|
integrationTestCompile 'org.assertj:assertj-core:${assertj_version}'
|
||||||
|
integrationTestCompile project(':test-utils')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package up the demo programs.
|
// Package up the demo programs.
|
||||||
@ -155,11 +158,10 @@ tasks.withType(CreateStartScripts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task integrationTest(type: Test) {
|
task integrationTest(type: Test, dependsOn: [':node:integrationTest',':client:integrationTest']) {
|
||||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||||
}
|
}
|
||||||
test.finalizedBy(integrationTest)
|
|
||||||
|
|
||||||
|
|
||||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||||
@ -196,46 +198,49 @@ applicationDistribution.into("bin") {
|
|||||||
fileMode = 0755
|
fileMode = 0755
|
||||||
}
|
}
|
||||||
|
|
||||||
task createCapsule(type: FatCapsule, dependsOn: 'quasarScan') {
|
task buildCordaJAR(type: FatCapsule, dependsOn: 'quasarScan') {
|
||||||
applicationClass 'com.r3corda.node.MainKt'
|
applicationClass 'com.r3corda.node.MainKt'
|
||||||
|
archiveName 'corda.jar'
|
||||||
|
applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class')
|
||||||
|
|
||||||
capsuleManifest {
|
capsuleManifest {
|
||||||
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
|
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
|
||||||
systemProperties['log4j.configuration'] = 'log4j2.xml'
|
systemProperties['log4j.configuration'] = 'log4j2.xml'
|
||||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
|
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
|
||||||
minJavaVersion = '1.8.0'
|
minJavaVersion = '1.8.0'
|
||||||
|
caplets = ['CordaCaplet']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task createStandalone(dependsOn: 'createCapsule') << {
|
task installTemplateNodes(dependsOn: 'buildCordaJAR') << {
|
||||||
copy {
|
copy {
|
||||||
from createCapsule.outputs.getFiles()
|
from buildCordaJAR.outputs.getFiles()
|
||||||
from 'config/dev/nameservernode.conf'
|
from 'config/dev/nameservernode.conf'
|
||||||
into "${buildDir}/standalone/nameserver"
|
into "${buildDir}/nodes/nameserver"
|
||||||
rename 'nameservernode.conf', 'node.conf'
|
rename 'nameservernode.conf', 'node.conf'
|
||||||
}
|
}
|
||||||
|
|
||||||
copy {
|
copy {
|
||||||
from createCapsule.outputs.getFiles()
|
from buildCordaJAR.outputs.getFiles()
|
||||||
from 'config/dev/generalnodea.conf'
|
from 'config/dev/generalnodea.conf'
|
||||||
into "${buildDir}/standalone/nodea"
|
into "${buildDir}/nodes/nodea"
|
||||||
rename 'generalnodea.conf', 'node.conf'
|
rename 'generalnodea.conf', 'node.conf'
|
||||||
}
|
}
|
||||||
|
|
||||||
copy {
|
copy {
|
||||||
from createCapsule.outputs.getFiles()
|
from buildCordaJAR.outputs.getFiles()
|
||||||
from 'config/dev/generalnodeb.conf'
|
from 'config/dev/generalnodeb.conf'
|
||||||
into "${buildDir}/standalone/nodeb"
|
into "${buildDir}/nodes/nodeb"
|
||||||
rename 'generalnodeb.conf', 'node.conf'
|
rename 'generalnodeb.conf', 'node.conf'
|
||||||
}
|
}
|
||||||
|
|
||||||
delete("${buildDir}/standalone/runstandalone")
|
delete("${buildDir}/nodes/runnodes")
|
||||||
def jarName = createCapsule.outputs.getFiles().getSingleFile().getName()
|
def jarName = buildCordaJAR.outputs.getFiles().getSingleFile().getName()
|
||||||
copy {
|
copy {
|
||||||
from "buildSrc/scripts/runstandalone"
|
from "buildSrc/scripts/runnodes"
|
||||||
filter { String line -> line.replace("JAR_NAME", jarName) }
|
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"))
|
filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf"))
|
||||||
into "${buildDir}/standalone"
|
into "${buildDir}/nodes"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
7
buildSrc/build.gradle
Normal file
7
buildSrc/build.gradle
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile "com.google.guava:guava:19.0"
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Creates three nodes. A network map and notary node and two regular nodes that can be extended with cordapps.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT
|
trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT
|
||||||
export CAPSULE_CACHE_DIR=cache
|
export CAPSULE_CACHE_DIR=cache
|
52
buildSrc/src/main/groovy/CanonicalizerPlugin.groovy
Normal file
52
buildSrc/src/main/groovy/CanonicalizerPlugin.groovy
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import com.google.common.io.ByteStreams
|
||||||
|
import org.gradle.api.*
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.attribute.FileTime
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
|
||||||
|
// Custom Gradle plugin that attempts to make the resulting jar file deterministic.
|
||||||
|
// Ie. same contract definition should result when compiled in same jar file.
|
||||||
|
// This is done by removing date time stamps from the files inside the jar.
|
||||||
|
class CanonicalizerPlugin implements Plugin<Project> {
|
||||||
|
void apply(Project project) {
|
||||||
|
|
||||||
|
project.getTasks().getByName('jar').doLast() {
|
||||||
|
|
||||||
|
def zipPath = (String) project.jar.archivePath
|
||||||
|
def destPath = Files.createTempFile("processzip", null)
|
||||||
|
|
||||||
|
def zeroTime = FileTime.fromMillis(0)
|
||||||
|
|
||||||
|
def input = new ZipFile(zipPath)
|
||||||
|
def entries = input.entries().toList().sort { it.name }
|
||||||
|
|
||||||
|
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
|
||||||
|
output.setMethod(ZipOutputStream.DEFLATED)
|
||||||
|
|
||||||
|
entries.each {
|
||||||
|
def newEntry = new ZipEntry( it.name )
|
||||||
|
|
||||||
|
newEntry.setLastModifiedTime(zeroTime)
|
||||||
|
newEntry.setCreationTime(zeroTime)
|
||||||
|
newEntry.compressedSize = -1
|
||||||
|
newEntry.size = it.size
|
||||||
|
newEntry.crc = it.crc
|
||||||
|
|
||||||
|
output.putNextEntry(newEntry)
|
||||||
|
|
||||||
|
ByteStreams.copy(input.getInputStream(it), output)
|
||||||
|
|
||||||
|
output.closeEntry()
|
||||||
|
}
|
||||||
|
output.close()
|
||||||
|
input.close()
|
||||||
|
|
||||||
|
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
73
client/build.gradle
Normal file
73
client/build.gradle
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: QuasarPlugin
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||||
|
}
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://dl.bintray.com/kotlin/exposed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//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
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
integrationTest {
|
||||||
|
kotlin {
|
||||||
|
compileClasspath += main.output + test.output
|
||||||
|
runtimeClasspath += main.output + test.output
|
||||||
|
srcDir file('src/integration-test/kotlin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
resources {
|
||||||
|
srcDir "../config/test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile project(':node')
|
||||||
|
|
||||||
|
// Log4J: logging framework (with SLF4J bindings)
|
||||||
|
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
|
||||||
|
compile "org.apache.logging.log4j:log4j-core:${log4j_version}"
|
||||||
|
|
||||||
|
compile "com.google.guava:guava:19.0"
|
||||||
|
|
||||||
|
// ReactFX: Functional reactive UI programming.
|
||||||
|
compile 'org.reactfx:reactfx:2.0-M5'
|
||||||
|
compile 'org.fxmisc.easybind:easybind:1.0.3'
|
||||||
|
|
||||||
|
// Unit testing helpers.
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||||
|
|
||||||
|
testCompile project(':test-utils')
|
||||||
|
|
||||||
|
// Integration test helpers
|
||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,251 @@
|
|||||||
|
package com.r3corda.client
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.serialization.OpaqueBytes
|
||||||
|
import com.r3corda.node.driver.driver
|
||||||
|
import com.r3corda.node.driver.startClient
|
||||||
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
|
import com.r3corda.node.services.monitor.TransactionBuildResult
|
||||||
|
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||||
|
import com.r3corda.node.utilities.AddOrRemove
|
||||||
|
import com.r3corda.testing.*
|
||||||
|
import org.junit.Test
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
val log: Logger = LoggerFactory.getLogger(WalletMonitorClientTests::class.java)
|
||||||
|
|
||||||
|
class WalletMonitorClientTests {
|
||||||
|
@Test
|
||||||
|
fun cashIssueWorksEndToEnd() {
|
||||||
|
driver {
|
||||||
|
val aliceNodeFuture = startNode("Alice")
|
||||||
|
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
|
||||||
|
|
||||||
|
val aliceNode = aliceNodeFuture.get()
|
||||||
|
val notaryNode = notaryNodeFuture.get()
|
||||||
|
val client = startClient(aliceNode).get()
|
||||||
|
|
||||||
|
log.info("Alice is ${aliceNode.identity}")
|
||||||
|
log.info("Notary is ${notaryNode.identity}")
|
||||||
|
|
||||||
|
val aliceInStream = PublishSubject.create<ServiceToClientEvent>()
|
||||||
|
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
|
||||||
|
|
||||||
|
val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
|
||||||
|
require(aliceMonitorClient.register().get())
|
||||||
|
|
||||||
|
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
|
||||||
|
amount = Amount(100, USD),
|
||||||
|
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
|
recipient = aliceNode.identity,
|
||||||
|
notary = notaryNode.identity
|
||||||
|
))
|
||||||
|
|
||||||
|
aliceInStream.expectEvents(isStrict = false) {
|
||||||
|
parallel(
|
||||||
|
expect { build: ServiceToClientEvent.TransactionBuild ->
|
||||||
|
val state = build.state
|
||||||
|
if (state is TransactionBuildResult.Failed) {
|
||||||
|
fail(state.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expect { output: ServiceToClientEvent.OutputState ->
|
||||||
|
require(output.consumed.size == 0)
|
||||||
|
require(output.produced.size == 1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun issueAndMoveWorks() {
|
||||||
|
driver {
|
||||||
|
val aliceNodeFuture = startNode("Alice")
|
||||||
|
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
|
||||||
|
|
||||||
|
val aliceNode = aliceNodeFuture.get()
|
||||||
|
val notaryNode = notaryNodeFuture.get()
|
||||||
|
val client = startClient(aliceNode).get()
|
||||||
|
|
||||||
|
log.info("Alice is ${aliceNode.identity}")
|
||||||
|
log.info("Notary is ${notaryNode.identity}")
|
||||||
|
|
||||||
|
val aliceInStream = PublishSubject.create<ServiceToClientEvent>()
|
||||||
|
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
|
||||||
|
|
||||||
|
val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
|
||||||
|
require(aliceMonitorClient.register().get())
|
||||||
|
|
||||||
|
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
|
||||||
|
amount = Amount(100, USD),
|
||||||
|
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
|
recipient = aliceNode.identity,
|
||||||
|
notary = notaryNode.identity
|
||||||
|
))
|
||||||
|
|
||||||
|
aliceOutStream.onNext(ClientToServiceCommand.PayCash(
|
||||||
|
amount = Amount(100, Issued(PartyAndReference(aliceNode.identity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||||
|
recipient = aliceNode.identity
|
||||||
|
))
|
||||||
|
|
||||||
|
aliceInStream.expectEvents {
|
||||||
|
sequence(
|
||||||
|
// ISSUE
|
||||||
|
parallel(
|
||||||
|
sequence(
|
||||||
|
expect { add: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(add.addOrRemove == AddOrRemove.ADD)
|
||||||
|
},
|
||||||
|
expect { remove: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(remove.addOrRemove == AddOrRemove.REMOVE)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
expect { tx: ServiceToClientEvent.Transaction ->
|
||||||
|
require(tx.transaction.tx.inputs.isEmpty())
|
||||||
|
require(tx.transaction.tx.outputs.size == 1)
|
||||||
|
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
||||||
|
// Only Alice signed
|
||||||
|
require(signaturePubKeys.size == 1)
|
||||||
|
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||||
|
},
|
||||||
|
expect { build: ServiceToClientEvent.TransactionBuild ->
|
||||||
|
val state = build.state
|
||||||
|
when (state) {
|
||||||
|
is TransactionBuildResult.ProtocolStarted -> {
|
||||||
|
}
|
||||||
|
is TransactionBuildResult.Failed -> fail(state.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expect { output: ServiceToClientEvent.OutputState ->
|
||||||
|
require(output.consumed.size == 0)
|
||||||
|
require(output.produced.size == 1)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
// MOVE
|
||||||
|
parallel(
|
||||||
|
sequence(
|
||||||
|
expect { add: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(add.addOrRemove == AddOrRemove.ADD)
|
||||||
|
},
|
||||||
|
expect { add: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(add.addOrRemove == AddOrRemove.REMOVE)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
expect { tx: ServiceToClientEvent.Transaction ->
|
||||||
|
require(tx.transaction.tx.inputs.size == 1)
|
||||||
|
require(tx.transaction.tx.outputs.size == 1)
|
||||||
|
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
||||||
|
// Alice and Notary signed
|
||||||
|
require(signaturePubKeys.size == 2)
|
||||||
|
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||||
|
require(signaturePubKeys.contains(notaryNode.identity.owningKey))
|
||||||
|
},
|
||||||
|
sequence(
|
||||||
|
expect { build: ServiceToClientEvent.TransactionBuild ->
|
||||||
|
val state = build.state
|
||||||
|
when (state) {
|
||||||
|
is TransactionBuildResult.ProtocolStarted -> {
|
||||||
|
log.info("${state.message}")
|
||||||
|
}
|
||||||
|
is TransactionBuildResult.Failed -> fail(state.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replicate(7) {
|
||||||
|
expect { build: ServiceToClientEvent.Progress -> }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
expect { output: ServiceToClientEvent.OutputState ->
|
||||||
|
require(output.consumed.size == 1)
|
||||||
|
require(output.produced.size == 1)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun movingCashOfDifferentIssueRefsFails() {
|
||||||
|
driver {
|
||||||
|
val aliceNodeFuture = startNode("Alice")
|
||||||
|
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
|
||||||
|
|
||||||
|
val aliceNode = aliceNodeFuture.get()
|
||||||
|
val notaryNode = notaryNodeFuture.get()
|
||||||
|
val client = startClient(aliceNode).get()
|
||||||
|
|
||||||
|
log.info("Alice is ${aliceNode.identity}")
|
||||||
|
log.info("Notary is ${notaryNode.identity}")
|
||||||
|
|
||||||
|
val aliceInStream = PublishSubject.create<ServiceToClientEvent>()
|
||||||
|
val aliceOutStream = PublishSubject.create<ClientToServiceCommand>()
|
||||||
|
|
||||||
|
val aliceMonitorClient = WalletMonitorClient(client, aliceNode, aliceOutStream, aliceInStream, PublishSubject.create())
|
||||||
|
require(aliceMonitorClient.register().get())
|
||||||
|
|
||||||
|
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
|
||||||
|
amount = Amount(100, USD),
|
||||||
|
issueRef = OpaqueBytes(ByteArray(1, { 1 })),
|
||||||
|
recipient = aliceNode.identity,
|
||||||
|
notary = notaryNode.identity
|
||||||
|
))
|
||||||
|
|
||||||
|
aliceOutStream.onNext(ClientToServiceCommand.IssueCash(
|
||||||
|
amount = Amount(100, USD),
|
||||||
|
issueRef = OpaqueBytes(ByteArray(1, { 2 })),
|
||||||
|
recipient = aliceNode.identity,
|
||||||
|
notary = notaryNode.identity
|
||||||
|
))
|
||||||
|
|
||||||
|
aliceOutStream.onNext(ClientToServiceCommand.PayCash(
|
||||||
|
amount = Amount(200, Issued(PartyAndReference(aliceNode.identity, OpaqueBytes(ByteArray(1, { 1 }))), USD)),
|
||||||
|
recipient = aliceNode.identity
|
||||||
|
))
|
||||||
|
|
||||||
|
aliceInStream.expectEvents {
|
||||||
|
sequence(
|
||||||
|
// ISSUE 1
|
||||||
|
parallel(
|
||||||
|
sequence(
|
||||||
|
expect { add: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(add.addOrRemove == AddOrRemove.ADD)
|
||||||
|
},
|
||||||
|
expect { remove: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(remove.addOrRemove == AddOrRemove.REMOVE)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
expect { tx: ServiceToClientEvent.Transaction -> },
|
||||||
|
expect { build: ServiceToClientEvent.TransactionBuild -> },
|
||||||
|
expect { output: ServiceToClientEvent.OutputState -> }
|
||||||
|
),
|
||||||
|
|
||||||
|
// ISSUE 2
|
||||||
|
parallel(
|
||||||
|
sequence(
|
||||||
|
expect { add: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(add.addOrRemove == AddOrRemove.ADD)
|
||||||
|
},
|
||||||
|
expect { remove: ServiceToClientEvent.StateMachine ->
|
||||||
|
require(remove.addOrRemove == AddOrRemove.REMOVE)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
expect { tx: ServiceToClientEvent.Transaction -> },
|
||||||
|
expect { build: ServiceToClientEvent.TransactionBuild -> },
|
||||||
|
expect { output: ServiceToClientEvent.OutputState -> }
|
||||||
|
),
|
||||||
|
|
||||||
|
// MOVE, should fail
|
||||||
|
expect { build: ServiceToClientEvent.TransactionBuild ->
|
||||||
|
val state = build.state
|
||||||
|
require(state is TransactionBuildResult.Failed)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package com.r3corda.client
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import com.google.common.util.concurrent.SettableFuture
|
||||||
|
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||||
|
import com.r3corda.core.messaging.MessagingService
|
||||||
|
import com.r3corda.core.node.NodeInfo
|
||||||
|
import com.r3corda.core.random63BitValue
|
||||||
|
import com.r3corda.core.serialization.deserialize
|
||||||
|
import com.r3corda.core.serialization.serialize
|
||||||
|
import com.r3corda.node.services.monitor.*
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Observer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Worked example of a client which communicates with the wallet monitor service.
|
||||||
|
*/
|
||||||
|
|
||||||
|
private val log: Logger = LoggerFactory.getLogger("WalletMonitorClient")
|
||||||
|
|
||||||
|
class WalletMonitorClient(
|
||||||
|
val net: MessagingService,
|
||||||
|
val node: NodeInfo,
|
||||||
|
val outEvents: Observable<ClientToServiceCommand>,
|
||||||
|
val inEvents: Observer<ServiceToClientEvent>,
|
||||||
|
val snapshot: Observer<StateSnapshotMessage>
|
||||||
|
) {
|
||||||
|
private val sessionID = random63BitValue()
|
||||||
|
|
||||||
|
fun register(): ListenableFuture<Boolean> {
|
||||||
|
|
||||||
|
val future = SettableFuture.create<Boolean>()
|
||||||
|
log.info("Registering with ID $sessionID. I am ${net.myAddress}")
|
||||||
|
net.addMessageHandler(WalletMonitorService.REGISTER_TOPIC, sessionID) { msg, reg ->
|
||||||
|
val resp = msg.data.deserialize<RegisterResponse>()
|
||||||
|
net.removeMessageHandler(reg)
|
||||||
|
future.set(resp.success)
|
||||||
|
}
|
||||||
|
net.addMessageHandler(WalletMonitorService.STATE_TOPIC, sessionID) { msg, reg ->
|
||||||
|
val snapshotMessage = msg.data.deserialize<StateSnapshotMessage>()
|
||||||
|
net.removeMessageHandler(reg)
|
||||||
|
snapshot.onNext(snapshotMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
net.addMessageHandler(WalletMonitorService.IN_EVENT_TOPIC, sessionID) { msg, reg ->
|
||||||
|
val event = msg.data.deserialize<ServiceToClientEvent>()
|
||||||
|
inEvents.onNext(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
val req = RegisterRequest(net.myAddress, sessionID)
|
||||||
|
val registerMessage = net.createMessage(WalletMonitorService.REGISTER_TOPIC, 0, req.serialize().bits)
|
||||||
|
net.send(registerMessage, node.address)
|
||||||
|
|
||||||
|
outEvents.subscribe { event ->
|
||||||
|
val envelope = ClientToServiceCommandMessage(sessionID, net.myAddress, event)
|
||||||
|
val message = net.createMessage(WalletMonitorService.OUT_EVENT_TOPIC, 0, envelope.serialize().bits)
|
||||||
|
net.send(message, node.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
return future
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import javafx.collections.ListChangeListener
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.collections.transformation.TransformationList
|
||||||
|
import kotlin.comparisons.compareValues
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an [ObservableList]<[E]> and a grouping key [K], [AggregatedList] groups the elements by the key into a fresh
|
||||||
|
* [ObservableList]<[E]> for each group and exposes the groups as an observable list of [A]s by calling [assemble] on each.
|
||||||
|
*
|
||||||
|
* Changes done to elements of the input list are reflected in the observable list of the respective group, whereas
|
||||||
|
* additions/removals of elements in the underlying list are reflected in the exposed [ObservableList]<[A]> by
|
||||||
|
* adding/deleting aggregations as expected.
|
||||||
|
*
|
||||||
|
* The ordering of the exposed list is based on the [hashCode] of keys.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* val statesGroupedByCurrency = AggregatedList(states, { state -> state.currency }) { currency, group ->
|
||||||
|
* object {
|
||||||
|
* val currency = currency
|
||||||
|
* val states = group
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* The above creates an observable list of (currency, statesOfCurrency) pairs.
|
||||||
|
*
|
||||||
|
* Note that update events to the source list are discarded, assuming the key of elements does not change.
|
||||||
|
* TODO Should we handle this case? It requires additional bookkeeping of sourceIndex->(aggregationIndex, groupIndex)
|
||||||
|
*
|
||||||
|
* @param list The underlying list.
|
||||||
|
* @param toKey Function to extract the key from an element.
|
||||||
|
* @param assemble Function to assemble the aggregation into the exposed [A].
|
||||||
|
*/
|
||||||
|
class AggregatedList<A, E, K : Any>(
|
||||||
|
list: ObservableList<out E>,
|
||||||
|
val toKey: (E) -> K,
|
||||||
|
val assemble: (K, ObservableList<E>) -> A
|
||||||
|
) : TransformationList<A, E>(list) {
|
||||||
|
|
||||||
|
private class AggregationGroup<E, out A>(
|
||||||
|
val keyHashCode: Int,
|
||||||
|
val value: A,
|
||||||
|
val elements: ObservableList<E>
|
||||||
|
)
|
||||||
|
|
||||||
|
// Invariant: sorted by K.hashCode()
|
||||||
|
private val aggregationList = mutableListOf<AggregationGroup<E, A>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
list.forEach { addItem(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(index: Int): A? = aggregationList.getOrNull(index)?.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We cannot implement this as aggregations are one to many
|
||||||
|
*/
|
||||||
|
override fun getSourceIndex(index: Int): Int {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val size: Int get() = aggregationList.size
|
||||||
|
|
||||||
|
override fun sourceChanged(c: ListChangeListener.Change<out E>) {
|
||||||
|
beginChange()
|
||||||
|
while (c.next()) {
|
||||||
|
if (c.wasPermutated()) {
|
||||||
|
// Permutation should not change aggregation
|
||||||
|
} else if (c.wasUpdated()) {
|
||||||
|
// Update should not change aggregation
|
||||||
|
} else {
|
||||||
|
for (removedSourceItem in c.removed) {
|
||||||
|
val removedPair = removeItem(removedSourceItem)
|
||||||
|
if (removedPair != null) {
|
||||||
|
nextRemove(removedPair.first, removedPair.second.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (addedItem in c.addedSubList) {
|
||||||
|
val insertIndex = addItem(addedItem)
|
||||||
|
if (insertIndex != null) {
|
||||||
|
nextAdd(insertIndex, insertIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeItem(removedItem: E): Pair<Int, AggregationGroup<E, A>>? {
|
||||||
|
val key = toKey(removedItem)
|
||||||
|
val keyHashCode = key.hashCode()
|
||||||
|
|
||||||
|
val index = aggregationList.binarySearch(
|
||||||
|
comparison = { group -> compareValues(keyHashCode, group.keyHashCode.hashCode()) }
|
||||||
|
)
|
||||||
|
if (index < 0) {
|
||||||
|
throw IllegalStateException("Removed element $removedItem does not map to an existing aggregation")
|
||||||
|
} else {
|
||||||
|
val aggregationGroup = aggregationList[index]
|
||||||
|
if (aggregationGroup.elements.size == 1) {
|
||||||
|
return Pair(index, aggregationList.removeAt(index))
|
||||||
|
}
|
||||||
|
aggregationGroup.elements.remove(removedItem)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addItem(addedItem: E): Int? {
|
||||||
|
val key = toKey(addedItem)
|
||||||
|
val keyHashCode = key.hashCode()
|
||||||
|
val index = aggregationList.binarySearch(
|
||||||
|
comparison = { group -> compareValues(keyHashCode, group.keyHashCode.hashCode()) }
|
||||||
|
)
|
||||||
|
if (index < 0) {
|
||||||
|
// New aggregation
|
||||||
|
val observableGroupElements = FXCollections.observableArrayList<E>()
|
||||||
|
observableGroupElements.add(addedItem)
|
||||||
|
val aggregationGroup = AggregationGroup(
|
||||||
|
keyHashCode = keyHashCode,
|
||||||
|
value = assemble(key, observableGroupElements),
|
||||||
|
elements = observableGroupElements
|
||||||
|
)
|
||||||
|
val insertIndex = -index - 1
|
||||||
|
aggregationList.add(insertIndex, aggregationGroup)
|
||||||
|
return insertIndex
|
||||||
|
} else {
|
||||||
|
aggregationList[index].elements.add(addedItem)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package com.r3corda.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 org.fxmisc.easybind.EasyBind
|
||||||
|
import java.util.*
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility bindings for the [Amount] type, similar in spirit to [Bindings]
|
||||||
|
*/
|
||||||
|
object AmountBindings {
|
||||||
|
fun <T> sum(amounts: ObservableList<Amount<T>>, token: T) = EasyBind.map(
|
||||||
|
Bindings.createLongBinding({
|
||||||
|
amounts.stream().collect(Collectors.summingLong {
|
||||||
|
require(it.token == token)
|
||||||
|
it.quantity
|
||||||
|
})
|
||||||
|
}, arrayOf(amounts))
|
||||||
|
) { sum -> Amount(sum.toLong(), token) }
|
||||||
|
|
||||||
|
fun exchange(
|
||||||
|
currency: ObservableValue<Currency>,
|
||||||
|
exchangeRate: ObservableValue<ExchangeRate>
|
||||||
|
): ObservableValue<Pair<Currency, (Amount<Currency>) -> Long>> {
|
||||||
|
return EasyBind.combine(currency, exchangeRate) { currency, exchangeRate ->
|
||||||
|
Pair(currency) { amount: Amount<Currency> ->
|
||||||
|
(exchangeRate.rate(amount.token, currency) * amount.quantity).toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sumAmountExchange(
|
||||||
|
amounts: ObservableList<Amount<Currency>>,
|
||||||
|
currency: ObservableValue<Currency>,
|
||||||
|
exchangeRate: ObservableValue<ExchangeRate>
|
||||||
|
): ObservableValue<Amount<Currency>> {
|
||||||
|
return EasyBind.monadic(exchange(currency, exchangeRate)).flatMap {
|
||||||
|
val (currencyValue, exchange: (Amount<Currency>) -> Long) = it
|
||||||
|
EasyBind.map(
|
||||||
|
Bindings.createLongBinding({
|
||||||
|
amounts.stream().collect(Collectors.summingLong { exchange(it) })
|
||||||
|
} , arrayOf(amounts))
|
||||||
|
) { Amount(it.toLong(), currencyValue) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.beans.Observable
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.ListChangeListener
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.collections.ObservableListBase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [ChosenList] manages an [ObservableList] that may be changed by the wrapping [ObservableValue]. Whenever the underlying
|
||||||
|
* [ObservableValue] changes the exposed list changes to the new value. Changes to the list are simply propagated.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* val filteredStates = ChosenList(EasyBind.map(filterCriteriaType) { type ->
|
||||||
|
* when (type) {
|
||||||
|
* is (ByCurrency) -> statesFilteredByCurrency
|
||||||
|
* is (ByIssuer) -> statesFilteredByIssuer
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* The above will create a list that chooses and delegates to the appropriate filtered list based on the type of filter.
|
||||||
|
*/
|
||||||
|
class ChosenList<E>(
|
||||||
|
private val chosenListObservable: ObservableValue<ObservableList<E>>
|
||||||
|
): ObservableListBase<E>() {
|
||||||
|
|
||||||
|
private var currentList = chosenListObservable.value
|
||||||
|
|
||||||
|
private val listener = object : ListChangeListener<E> {
|
||||||
|
override fun onChanged(change: ListChangeListener.Change<out E>) = fireChange(change)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
chosenListObservable.addListener { observable: Observable -> rechoose() }
|
||||||
|
currentList.addListener(listener)
|
||||||
|
beginChange()
|
||||||
|
nextAdd(0, currentList.size)
|
||||||
|
endChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(index: Int) = currentList.get(index)
|
||||||
|
override val size: Int get() = currentList.size
|
||||||
|
|
||||||
|
private fun rechoose() {
|
||||||
|
val chosenList = chosenListObservable.value
|
||||||
|
if (currentList != chosenList) {
|
||||||
|
pick(chosenList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pick(list: ObservableList<E>) {
|
||||||
|
currentList.removeListener(listener)
|
||||||
|
list.addListener(listener)
|
||||||
|
beginChange()
|
||||||
|
nextRemove(0, currentList)
|
||||||
|
currentList = list
|
||||||
|
nextAdd(0, list.size)
|
||||||
|
endChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.application.Platform
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple utilities for converting an [rx.Observable] into a javafx [ObservableValue]/[ObservableList]
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun <A, B> Observable<A>.foldToObservableValue(initial: B, folderFun: (A, B) -> B): ObservableValue<B> {
|
||||||
|
val result = SimpleObjectProperty<B>(initial)
|
||||||
|
subscribe {
|
||||||
|
Platform.runLater {
|
||||||
|
result.set(folderFun(it, result.get()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <A, B, C> Observable<A>.foldToObservableList(
|
||||||
|
initialAccumulator: C, folderFun: (A, C, ObservableList<B>) -> C
|
||||||
|
): ObservableList<B> {
|
||||||
|
val result = FXCollections.observableArrayList<B>()
|
||||||
|
/**
|
||||||
|
* This capture is fine, as [Platform.runLater] runs closures in order
|
||||||
|
*/
|
||||||
|
var currentAccumulator = initialAccumulator
|
||||||
|
subscribe {
|
||||||
|
Platform.runLater {
|
||||||
|
currentAccumulator = folderFun(it, currentAccumulator, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
40
client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt
Normal file
40
client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package com.r3corda.client.mock
|
||||||
|
|
||||||
|
class ErrorOr<out A> private constructor(
|
||||||
|
val value: A?,
|
||||||
|
val error: Exception?
|
||||||
|
) {
|
||||||
|
constructor(value: A): this(value, null)
|
||||||
|
constructor(error: Exception): this(null, error)
|
||||||
|
|
||||||
|
fun <T> match(onValue: (A) -> T, onError: (Exception) -> T): T {
|
||||||
|
if (value != null) {
|
||||||
|
return onValue(value)
|
||||||
|
} else {
|
||||||
|
return onError(error!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getValueOrThrow(): A {
|
||||||
|
if (value != null) {
|
||||||
|
return value
|
||||||
|
} else {
|
||||||
|
throw error!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Functor
|
||||||
|
fun <B> map(function: (A) -> B): ErrorOr<B> {
|
||||||
|
return 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>): ErrorOr<B> {
|
||||||
|
return value?.let(function) ?: ErrorOr<B>(error!!)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
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 com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
|
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 wallet = 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()
|
||||||
|
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 { wallet }.bind { states ->
|
||||||
|
Generator.sampleBernoulli(states, 0.2).map { someStates ->
|
||||||
|
val consumedSet = someStates.map { it.ref }.toSet()
|
||||||
|
wallet = wallet.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 { wallet }.bind { states ->
|
||||||
|
Generator.replicate(2, cashStateGenerator).map {
|
||||||
|
wallet = states + it
|
||||||
|
it.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val outputStateGenerator = consumedGenerator.combine(producedGenerator) { consumed, produced ->
|
||||||
|
ServiceToClientEvent.OutputState(Instant.now(), consumed, produced)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 serviceToClientEventGenerator = Generator.frequency<ServiceToClientEvent>(
|
||||||
|
1.0 to outputStateGenerator
|
||||||
|
)
|
||||||
|
|
||||||
|
val clientToServiceCommandGenerator = Generator.frequency(
|
||||||
|
0.33 to issueCashGenerator,
|
||||||
|
0.33 to moveCashGenerator
|
||||||
|
)
|
||||||
|
}
|
171
client/src/main/kotlin/com/r3corda/client/mock/Generator.kt
Normal file
171
client/src/main/kotlin/com/r3corda/client/mock/Generator.kt
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
package com.r3corda.client.mock
|
||||||
|
|
||||||
|
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(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)
|
||||||
|
if (element.value != null) {
|
||||||
|
result.add(element.value)
|
||||||
|
} else {
|
||||||
|
return@Generator ErrorOr(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: Exception? = null
|
||||||
|
for (i in 0 .. numberOfTries - 1) {
|
||||||
|
val result = generate(random)
|
||||||
|
if (result.value != null) {
|
||||||
|
return result.value
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorOr.error != null) {
|
||||||
|
return@Generator ErrorOr(errorOr.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package com.r3corda.client.model
|
||||||
|
|
||||||
|
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.client.fxutils.foldToObservableList
|
||||||
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
|
import com.r3corda.node.services.monitor.StateSnapshotMessage
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import kotlinx.support.jdk8.collections.removeIf
|
||||||
|
import rx.Observable
|
||||||
|
|
||||||
|
class StatesDiff<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 serviceToClient: Observable<ServiceToClientEvent> by observable(WalletMonitorModel::serviceToClient)
|
||||||
|
private val snapshot: Observable<StateSnapshotMessage> by observable(WalletMonitorModel::snapshot)
|
||||||
|
private val outputStates = serviceToClient.ofType(ServiceToClientEvent.OutputState::class.java)
|
||||||
|
|
||||||
|
val contractStatesDiff = outputStates.map { StatesDiff(it.produced, it.consumed) }
|
||||||
|
// We filter the diff first rather than the complete contract state list.
|
||||||
|
// TODO wire up snapshot once it holds StateAndRefs
|
||||||
|
val cashStatesDiff = contractStatesDiff.map {
|
||||||
|
StatesDiff(it.added.filterIsInstance<StateAndRef<Cash.State>>(), 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.r3corda.client.model
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.Amount
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This model provides an exchange rate from arbitrary currency to arbitrary currency.
|
||||||
|
* TODO hook up an actual oracle
|
||||||
|
*/
|
||||||
|
class ExchangeRateModel {
|
||||||
|
val exchangeRate: ObservableValue<ExchangeRate> = SimpleObjectProperty<ExchangeRate>(object : ExchangeRate {
|
||||||
|
override fun rate(from: Currency, to: Currency) = 1.0
|
||||||
|
})
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
package com.r3corda.client.model
|
||||||
|
|
||||||
|
import com.r3corda.client.fxutils.foldToObservableList
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
|
import com.r3corda.node.services.monitor.TransactionBuildResult
|
||||||
|
import com.r3corda.node.utilities.AddOrRemove
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import rx.Observable
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface GatheredTransactionData {
|
||||||
|
val fiberId: ObservableValue<Long?>
|
||||||
|
val uuid: ObservableValue<UUID?>
|
||||||
|
val protocolName: ObservableValue<String?>
|
||||||
|
val protocolStatus: ObservableValue<ProtocolStatus?>
|
||||||
|
val transaction: ObservableValue<SignedTransaction?>
|
||||||
|
val status: ObservableValue<TransactionCreateStatus?>
|
||||||
|
val lastUpdate: ObservableValue<Instant>
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ProtocolStatus(val status: String?) {
|
||||||
|
object Added: ProtocolStatus(null)
|
||||||
|
object Removed: ProtocolStatus(null)
|
||||||
|
class InProgress(status: String): ProtocolStatus(status)
|
||||||
|
override fun toString(): String = status ?: javaClass.simpleName
|
||||||
|
}
|
||||||
|
|
||||||
|
data class GatheredTransactionDataWritable(
|
||||||
|
override val fiberId: SimpleObjectProperty<Long?> = SimpleObjectProperty(null),
|
||||||
|
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null),
|
||||||
|
override val protocolName: SimpleObjectProperty<String?> = SimpleObjectProperty(null),
|
||||||
|
override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null),
|
||||||
|
override val transaction: SimpleObjectProperty<SignedTransaction?> = SimpleObjectProperty(null),
|
||||||
|
override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null),
|
||||||
|
override val lastUpdate: SimpleObjectProperty<Instant>
|
||||||
|
) : GatheredTransactionData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This model provides an observable list of states relating to the creation of a transaction not yet on ledger.
|
||||||
|
*/
|
||||||
|
class GatheredTransactionDataModel {
|
||||||
|
|
||||||
|
private val serviceToClient: Observable<ServiceToClientEvent> by observable(WalletMonitorModel::serviceToClient)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregation of updates to transactions. We use the observable list as the only container and do linear search for
|
||||||
|
* matching transactions because we have two keys(fiber ID and UUID) and this way it's easier to avoid syncing issues.
|
||||||
|
*
|
||||||
|
* The Fiber ID is used to identify events that relate to the same transaction server-side, whereas the UUID is
|
||||||
|
* generated on the UI and is used to identify events with the UI action that triggered them. Currently a UUID is
|
||||||
|
* generated for each outgoing [ClientToServiceCommand].
|
||||||
|
*
|
||||||
|
* TODO: Make this more efficient by maintaining and syncing two maps (for the two keys) in the accumulator
|
||||||
|
* (Note that a transaction may be mapped by one or both)
|
||||||
|
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
|
||||||
|
*/
|
||||||
|
val gatheredGatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
||||||
|
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, Unit>(
|
||||||
|
initialAccumulator = Unit,
|
||||||
|
folderFun = { serviceToClientEvent, _unit, transactionStates ->
|
||||||
|
return@foldToObservableList when (serviceToClientEvent) {
|
||||||
|
is ServiceToClientEvent.Transaction -> {
|
||||||
|
// TODO handle this once we have some id to associate the tx with
|
||||||
|
}
|
||||||
|
is ServiceToClientEvent.OutputState -> {}
|
||||||
|
is ServiceToClientEvent.StateMachine -> {
|
||||||
|
newFiberIdTransactionStateOrModify(transactionStates,
|
||||||
|
fiberId = serviceToClientEvent.fiberId,
|
||||||
|
lastUpdate = serviceToClientEvent.time,
|
||||||
|
tweak = {
|
||||||
|
protocolName.set(serviceToClientEvent.label)
|
||||||
|
protocolStatus.set(when (serviceToClientEvent.addOrRemove) {
|
||||||
|
AddOrRemove.ADD -> ProtocolStatus.Added
|
||||||
|
AddOrRemove.REMOVE -> ProtocolStatus.Removed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ServiceToClientEvent.Progress -> {
|
||||||
|
newFiberIdTransactionStateOrModify(transactionStates,
|
||||||
|
fiberId = serviceToClientEvent.fiberId,
|
||||||
|
lastUpdate = serviceToClientEvent.time,
|
||||||
|
tweak = {
|
||||||
|
protocolStatus.set(ProtocolStatus.InProgress(serviceToClientEvent.message))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ServiceToClientEvent.TransactionBuild -> {
|
||||||
|
val state = serviceToClientEvent.state
|
||||||
|
newUuidTransactionStateOrModify(transactionStates,
|
||||||
|
uuid = serviceToClientEvent.id,
|
||||||
|
fiberId = when (state) {
|
||||||
|
is TransactionBuildResult.ProtocolStarted -> state.fiberId
|
||||||
|
is TransactionBuildResult.Failed -> null
|
||||||
|
},
|
||||||
|
lastUpdate = serviceToClientEvent.time,
|
||||||
|
tweak = {
|
||||||
|
return@newUuidTransactionStateOrModify when (state) {
|
||||||
|
is TransactionBuildResult.ProtocolStarted -> {
|
||||||
|
transaction.set(state.transaction)
|
||||||
|
status.set(TransactionCreateStatus.Started(state.message))
|
||||||
|
}
|
||||||
|
is TransactionBuildResult.Failed -> {
|
||||||
|
status.set(TransactionCreateStatus.Failed(state.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun newFiberIdTransactionStateOrModify(
|
||||||
|
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||||
|
fiberId: Long,
|
||||||
|
lastUpdate: Instant,
|
||||||
|
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||||
|
) {
|
||||||
|
val index = transactionStates.indexOfFirst { it.fiberId.value == fiberId }
|
||||||
|
if (index < 0) {
|
||||||
|
val newState = GatheredTransactionDataWritable(
|
||||||
|
fiberId = SimpleObjectProperty(fiberId),
|
||||||
|
lastUpdate = SimpleObjectProperty(lastUpdate)
|
||||||
|
)
|
||||||
|
tweak(newState)
|
||||||
|
transactionStates.add(newState)
|
||||||
|
} else {
|
||||||
|
val existingState = transactionStates[index]
|
||||||
|
existingState.lastUpdate.set(lastUpdate)
|
||||||
|
tweak(existingState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newUuidTransactionStateOrModify(
|
||||||
|
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||||
|
uuid: UUID,
|
||||||
|
fiberId: Long?,
|
||||||
|
lastUpdate: Instant,
|
||||||
|
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||||
|
) {
|
||||||
|
val index = transactionStates.indexOfFirst {
|
||||||
|
it.uuid.value == uuid || (fiberId != null && it.fiberId.value == fiberId)
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
val newState = GatheredTransactionDataWritable(
|
||||||
|
uuid = SimpleObjectProperty(uuid),
|
||||||
|
fiberId = SimpleObjectProperty(fiberId),
|
||||||
|
lastUpdate = SimpleObjectProperty(lastUpdate)
|
||||||
|
)
|
||||||
|
tweak(newState)
|
||||||
|
transactionStates.add(newState)
|
||||||
|
} else {
|
||||||
|
val existingState = transactionStates[index]
|
||||||
|
existingState.lastUpdate.set(lastUpdate)
|
||||||
|
tweak(existingState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
166
client/src/main/kotlin/com/r3corda/client/model/Models.kt
Normal file
166
client/src/main/kotlin/com/r3corda/client/model/Models.kt
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package com.r3corda.client.model
|
||||||
|
|
||||||
|
import javafx.beans.property.ObjectProperty
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
|
import javafx.beans.value.WritableValue
|
||||||
|
import javafx.collections.ObservableList
|
||||||
|
import org.reactfx.EventSink
|
||||||
|
import org.reactfx.EventStream
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Observer
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file defines a global [Models] store and delegates to inject event streams/sinks. Note that all streams here
|
||||||
|
* are global.
|
||||||
|
*
|
||||||
|
* This allows decoupling of UI logic from stream initialisation and provides us with a central place to inspect data
|
||||||
|
* flows. It also allows detecting of looping logic by constructing a stream dependency graph TODO do this.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* // Inject service -> client event stream
|
||||||
|
* private val serviceToClient: EventStream<ServiceToClientEvent> by eventStream(WalletMonitorModel::serviceToClient)
|
||||||
|
*
|
||||||
|
* Each Screen code should have a code layout like this:
|
||||||
|
*
|
||||||
|
* class Screen {
|
||||||
|
* val root = (..)
|
||||||
|
*
|
||||||
|
* [ inject UI elements using fxid()/inject() ]
|
||||||
|
*
|
||||||
|
* [ inject observable dependencies using observable()/eventSink() etc]
|
||||||
|
*
|
||||||
|
* [ define screen-specific observables ]
|
||||||
|
*
|
||||||
|
* init {
|
||||||
|
* [ wire up UI elements ]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* For example if I wanted to display a list of all USD cash states:
|
||||||
|
* class USDCashStatesScreen {
|
||||||
|
* val root: Pane by fxml()
|
||||||
|
*
|
||||||
|
* val usdCashStatesListView: ListView<Cash.State> by fxid("USDCashStatesListView")
|
||||||
|
*
|
||||||
|
* val cashStates: ObservableList<Cash.State> by observableList(ContractStateModel::cashStates)
|
||||||
|
*
|
||||||
|
* val usdCashStates = cashStates.filter { it.(..).currency == USD }
|
||||||
|
*
|
||||||
|
* init {
|
||||||
|
* Bindings.bindContent(usdCashStatesListView.items, usdCashStates)
|
||||||
|
* usdCashStatesListView.setCellValueFactory(somethingsomething)
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* The UI code can just assume that the cash state list comes from somewhere outside. The initialisation of that
|
||||||
|
* observable is decoupled, it may be mocked or be streamed from the network etc.
|
||||||
|
*
|
||||||
|
* Later on we may even want to move all screen-specific observables to a separate Model as well (like usdCashStates) - this
|
||||||
|
* would allow moving all of the aggregation logic to e.g. a different machine, all the UI will do is inject these and wire
|
||||||
|
* them up with the UI elements.
|
||||||
|
*
|
||||||
|
* Another advantage of this separation is that once we start adding a lot of screens we can still track data dependencies
|
||||||
|
* in a central place as opposed to ad-hoc wiring up the observables.
|
||||||
|
*/
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observable(noinline observableProperty: (M) -> Observable<T>) =
|
||||||
|
TrackedDelegate.ObservableDelegate(M::class, observableProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observer(noinline observerProperty: (M) -> Observer<T>) =
|
||||||
|
TrackedDelegate.ObserverDelegate(M::class, observerProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> eventStream(noinline streamProperty: (M) -> EventStream<T>) =
|
||||||
|
TrackedDelegate.EventStreamDelegate(M::class, streamProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> eventSink(noinline sinkProperty: (M) -> EventSink<T>) =
|
||||||
|
TrackedDelegate.EventSinkDelegate(M::class, sinkProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observableValue(noinline observableValueProperty: (M) -> ObservableValue<T>) =
|
||||||
|
TrackedDelegate.ObservableValueDelegate(M::class, observableValueProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> writableValue(noinline writableValueProperty: (M) -> WritableValue<T>) =
|
||||||
|
TrackedDelegate.WritableValueDelegate(M::class, writableValueProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> objectProperty(noinline objectProperty: (M) -> ObjectProperty<T>) =
|
||||||
|
TrackedDelegate.ObjectPropertyDelegate(M::class, objectProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observableList(noinline observableListProperty: (M) -> ObservableList<T>) =
|
||||||
|
TrackedDelegate.ObservableListDelegate(M::class, observableListProperty)
|
||||||
|
|
||||||
|
inline fun <reified M : Any, T> observableListReadOnly(noinline observableListProperty: (M) -> ObservableList<out T>) =
|
||||||
|
TrackedDelegate.ObservableListReadOnlyDelegate(M::class, observableListProperty)
|
||||||
|
|
||||||
|
object Models {
|
||||||
|
private val modelStore = HashMap<KClass<*>, Any>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a class->dependencies map that tracks what screens are depending on what model.
|
||||||
|
*/
|
||||||
|
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 {
|
||||||
|
dependencyGraph.getOrPut(origin) { mutableSetOf<KClass<*>>() }.add(klass)
|
||||||
|
val model = initModel(klass)
|
||||||
|
if (model.javaClass != klass.java) {
|
||||||
|
throw IllegalStateException("Model stored as ${klass.qualifiedName} has type ${model.javaClass}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return model as M
|
||||||
|
}
|
||||||
|
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) }
|
||||||
|
|
||||||
|
class ObservableDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> Observable<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): Observable<T> {
|
||||||
|
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ObserverDelegate<M : Any, T> (klass: KClass<M>, val eventStreamProperty: (M) -> Observer<T>) : TrackedDelegate<M>(klass) {
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): Observer<T> {
|
||||||
|
return eventStreamProperty(Models.get(klass, thisRef.javaClass.kotlin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
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,42 @@
|
|||||||
|
package com.r3corda.client.model
|
||||||
|
|
||||||
|
import com.r3corda.client.WalletMonitorClient
|
||||||
|
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||||
|
import com.r3corda.core.messaging.MessagingService
|
||||||
|
import com.r3corda.core.node.NodeInfo
|
||||||
|
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||||
|
import com.r3corda.node.services.monitor.StateSnapshotMessage
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Observer
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This model exposes raw event streams to and from the [WalletMonitorService] through a [WalletMonitorClient]
|
||||||
|
*/
|
||||||
|
class WalletMonitorModel {
|
||||||
|
private val clientToServiceSource = PublishSubject.create<ClientToServiceCommand>()
|
||||||
|
val clientToService: Observer<ClientToServiceCommand> = clientToServiceSource
|
||||||
|
|
||||||
|
private val serviceToClientSource = PublishSubject.create<ServiceToClientEvent>()
|
||||||
|
val serviceToClient: Observable<ServiceToClientEvent> = serviceToClientSource
|
||||||
|
|
||||||
|
private val snapshotSource = PublishSubject.create<StateSnapshotMessage>()
|
||||||
|
val snapshot: Observable<StateSnapshotMessage> = snapshotSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register for updates to/from a given wallet.
|
||||||
|
* @param messagingService The messaging to use for communication.
|
||||||
|
* @param walletMonitorNodeInfo the [Node] to connect to.
|
||||||
|
* TODO provide an unsubscribe mechanism
|
||||||
|
*/
|
||||||
|
fun register(messagingService: MessagingService, walletMonitorNodeInfo: NodeInfo) {
|
||||||
|
val monitorClient = WalletMonitorClient(
|
||||||
|
messagingService,
|
||||||
|
walletMonitorNodeInfo,
|
||||||
|
clientToServiceSource,
|
||||||
|
serviceToClientSource,
|
||||||
|
snapshotSource
|
||||||
|
)
|
||||||
|
require(monitorClient.register().get())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package com.r3corda.client.fxutils
|
||||||
|
|
||||||
|
import javafx.collections.FXCollections
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
class AggregatedListTest {
|
||||||
|
|
||||||
|
var sourceList = FXCollections.observableArrayList<Int>()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
sourceList = FXCollections.observableArrayList<Int>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun addWorks() {
|
||||||
|
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||||
|
require(aggregatedList.size == 0) { "Aggregation is empty is source list is" }
|
||||||
|
|
||||||
|
sourceList.add(9)
|
||||||
|
require(aggregatedList.size == 1) { "Aggregation list has one element if one was added to source list" }
|
||||||
|
require(aggregatedList[0]!!.first == 0)
|
||||||
|
|
||||||
|
sourceList.add(8)
|
||||||
|
require(aggregatedList.size == 2) { "Aggregation list has two elements if two were added to source list with different keys" }
|
||||||
|
|
||||||
|
sourceList.add(6)
|
||||||
|
require(aggregatedList.size == 2) { "Aggregation list's size doesn't change if element with existing key is added" }
|
||||||
|
|
||||||
|
aggregatedList.forEach {
|
||||||
|
when (it.first) {
|
||||||
|
0 -> require(it.second.toSet() == setOf(6, 9))
|
||||||
|
2 -> require(it.second.size == 1)
|
||||||
|
else -> fail("No aggregation expected with key ${it.first}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeWorks() {
|
||||||
|
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||||
|
sourceList.addAll(0, 1, 2, 3, 4)
|
||||||
|
|
||||||
|
require(aggregatedList.size == 3)
|
||||||
|
aggregatedList.forEach {
|
||||||
|
when (it.first) {
|
||||||
|
0 -> require(it.second.toSet() == setOf(0, 3))
|
||||||
|
1 -> require(it.second.toSet() == setOf(1, 4))
|
||||||
|
2 -> require(it.second.toSet() == setOf(2))
|
||||||
|
else -> fail("No aggregation expected with key ${it.first}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceList.remove(4)
|
||||||
|
require(aggregatedList.size == 3)
|
||||||
|
aggregatedList.forEach {
|
||||||
|
when (it.first) {
|
||||||
|
0 -> require(it.second.toSet() == setOf(0, 3))
|
||||||
|
1 -> require(it.second.toSet() == setOf(1))
|
||||||
|
2 -> require(it.second.toSet() == setOf(2))
|
||||||
|
else -> fail("No aggregation expected with key ${it.first}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceList.remove(2, 4)
|
||||||
|
require(aggregatedList.size == 2)
|
||||||
|
aggregatedList.forEach {
|
||||||
|
when (it.first) {
|
||||||
|
0 -> require(it.second.toSet() == setOf(0))
|
||||||
|
1 -> require(it.second.toSet() == setOf(1))
|
||||||
|
else -> fail("No aggregation expected with key ${it.first}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceList.removeAll(0, 1)
|
||||||
|
require(aggregatedList.size == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
13
config/dev/corda_dev_ca.cer
Normal file
13
config/dev/corda_dev_ca.cer
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICCjCCAbCgAwIBAgIINo1Qd4LJ1RUwCgYIKoZIzj0EAwIwWDEbMBkGA1UEAwwS
|
||||||
|
Q29yZGEgTm9kZSBSb290IENBMQswCQYDVQQKDAJSMzEOMAwGA1UECwwFY29yZGEx
|
||||||
|
DzANBgNVBAcMBkxvbmRvbjELMAkGA1UEBhMCVUswHhcNMTYwNzE5MDAwMDAwWhcN
|
||||||
|
MjYwNzE3MDAwMDAwWjBYMRswGQYDVQQDDBJDb3JkYSBOb2RlIFJvb3QgQ0ExCzAJ
|
||||||
|
BgNVBAoMAlIzMQ4wDAYDVQQLDAVjb3JkYTEPMA0GA1UEBwwGTG9uZG9uMQswCQYD
|
||||||
|
VQQGEwJVSzBWMBAGByqGSM49AgEGBSuBBAAKA0IABHtaVzCVCvNqp+Jhy5/hC25h
|
||||||
|
yHomwW5gJpNCPUdgVpLnSUXm+NRzf0ia+1SevkaEPSf5kzk47K1Po6KBWVTPbUaj
|
||||||
|
ZzBlMB0GA1UdDgQWBBSNU2aUShmUQCadxpzs+WWMQTSMJTASBgNVHRMBAf8ECDAG
|
||||||
|
AQH/AgECMAsGA1UdDwQEAwIBtjAjBgNVHSUEHDAaBggrBgEFBQcDAQYIKwYBBQUH
|
||||||
|
AwIGBFUdJQAwCgYIKoZIzj0EAwIDSAAwRQIgaHrsbm0ZZ5uFXCfntTcDddJmttiX
|
||||||
|
IXi6aN7mIIsl/5kCIQC4RCyclxoeZD/TaraTDzOVeSaooxLA/SrwQMzd0pxCcg==
|
||||||
|
-----END CERTIFICATE-----
|
@ -1,14 +1,11 @@
|
|||||||
basedir : "./nodea",
|
basedir : "./nodea"
|
||||||
myLegalName : "Bank A",
|
myLegalName : "Bank A"
|
||||||
nearestCity : "London",
|
nearestCity : "London"
|
||||||
keyStorePassword : "cordacadevpass",
|
keyStorePassword : "cordacadevpass"
|
||||||
trustStorePassword : "trustpass",
|
trustStorePassword : "trustpass"
|
||||||
artemisAddress : "localhost:31337",
|
artemisAddress : "localhost:31337"
|
||||||
webAddress : "localhost:31339",
|
webAddress : "localhost:31339"
|
||||||
hostNotaryServiceLocally: false,
|
hostNotaryServiceLocally: false
|
||||||
extraAdvertisedServiceIds: "corda.interest_rates",
|
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||||
mapService : {
|
networkMapAddress : "localhost:12345"
|
||||||
hostServiceLocally : false,
|
useHTTPS : false
|
||||||
address : "localhost:12345",
|
|
||||||
identity : "Notary Service"
|
|
||||||
}
|
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
basedir : "./nodeb",
|
basedir : "./nodeb"
|
||||||
myLegalName : "Bank B",
|
myLegalName : "Bank B"
|
||||||
nearestCity : "London",
|
nearestCity : "London"
|
||||||
keyStorePassword : "cordacadevpass",
|
keyStorePassword : "cordacadevpass"
|
||||||
trustStorePassword : "trustpass",
|
trustStorePassword : "trustpass"
|
||||||
artemisAddress : "localhost:31338",
|
artemisAddress : "localhost:31338"
|
||||||
webAddress : "localhost:31340",
|
webAddress : "localhost:31340"
|
||||||
hostNotaryServiceLocally: false,
|
hostNotaryServiceLocally: false
|
||||||
extraAdvertisedServiceIds: "corda.interest_rates",
|
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||||
mapService : {
|
networkMapAddress : "localhost:12345"
|
||||||
hostServiceLocally : false,
|
useHTTPS : false
|
||||||
address : "localhost:12345",
|
|
||||||
identity : "Notary Service"
|
|
||||||
}
|
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
basedir : "./nameserver",
|
basedir : "./nameserver"
|
||||||
myLegalName : "Notary Service",
|
myLegalName : "Notary Service"
|
||||||
nearestCity : "London",
|
nearestCity : "London"
|
||||||
keyStorePassword : "cordacadevpass",
|
keyStorePassword : "cordacadevpass"
|
||||||
trustStorePassword : "trustpass",
|
trustStorePassword : "trustpass"
|
||||||
artemisAddress : "localhost:12345",
|
artemisAddress : "localhost:12345"
|
||||||
webAddress : "localhost:12346",
|
webAddress : "localhost:12346"
|
||||||
hostNotaryServiceLocally: true,
|
hostNotaryServiceLocally: true
|
||||||
extraAdvertisedServiceIds: "",
|
extraAdvertisedServiceIds: ""
|
||||||
mapService : {
|
useHTTPS : false
|
||||||
hostServiceLocally : true,
|
|
||||||
address : ${artemisAddress},
|
|
||||||
identity : ${myLegalName}
|
|
||||||
}
|
|
@ -1,83 +1,22 @@
|
|||||||
import com.google.common.io.ByteStreams
|
|
||||||
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
import java.nio.file.attribute.FileTime
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath "com.google.guava:guava:19.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Gradle plugin that attempts to make the resulting jar file deterministic.
|
|
||||||
// Ie. same contract definition should result when compiled in same jar file.
|
|
||||||
// This is done by removing date time stamps from the files inside the jar.
|
|
||||||
class CanonicalizerPlugin implements Plugin<Project> {
|
|
||||||
void apply(Project project) {
|
|
||||||
|
|
||||||
project.getTasks().getByName('jar').doLast() {
|
|
||||||
|
|
||||||
def zipPath = (String) project.jar.archivePath
|
|
||||||
def destPath = Files.createTempFile("processzip", null)
|
|
||||||
|
|
||||||
def zeroTime = FileTime.fromMillis(0)
|
|
||||||
|
|
||||||
def input = new ZipFile(zipPath)
|
|
||||||
def entries = input.entries().toList().sort { it.name }
|
|
||||||
|
|
||||||
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
|
|
||||||
output.setMethod(ZipOutputStream.DEFLATED)
|
|
||||||
|
|
||||||
entries.each {
|
|
||||||
def newEntry = new ZipEntry(it.name)
|
|
||||||
|
|
||||||
newEntry.setLastModifiedTime(zeroTime)
|
|
||||||
newEntry.setCreationTime(zeroTime)
|
|
||||||
newEntry.compressedSize = -1
|
|
||||||
newEntry.size = it.size
|
|
||||||
newEntry.crc = it.crc
|
|
||||||
|
|
||||||
output.putNextEntry(newEntry)
|
|
||||||
|
|
||||||
ByteStreams.copy(input.getInputStream(it), output)
|
|
||||||
|
|
||||||
output.closeEntry()
|
|
||||||
}
|
|
||||||
output.close()
|
|
||||||
input.close()
|
|
||||||
|
|
||||||
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
|
|
||||||
apply plugin: CanonicalizerPlugin
|
apply plugin: CanonicalizerPlugin
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven {
|
maven {
|
||||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||||
}
|
}
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://dl.bintray.com/kotlin/exposed'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':core')
|
compile project(':core')
|
||||||
|
|
||||||
|
testCompile project(':test-utils')
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,68 +1,4 @@
|
|||||||
import com.google.common.io.ByteStreams
|
|
||||||
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
import java.nio.file.attribute.FileTime
|
|
||||||
import java.util.zip.ZipEntry
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
import java.util.zip.ZipOutputStream
|
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath "com.google.guava:guava:19.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Gradle plugin that attempts to make the resulting jar file deterministic.
|
|
||||||
// Ie. same contract definition should result when compiled in same jar file.
|
|
||||||
// This is done by removing date time stamps from the files inside the jar.
|
|
||||||
class CanonicalizerPlugin implements Plugin<Project> {
|
|
||||||
void apply(Project project) {
|
|
||||||
|
|
||||||
project.getTasks().getByName('jar').doLast() {
|
|
||||||
|
|
||||||
def zipPath = (String) project.jar.archivePath
|
|
||||||
def destPath = Files.createTempFile("processzip", null)
|
|
||||||
|
|
||||||
def zeroTime = FileTime.fromMillis(0)
|
|
||||||
|
|
||||||
def input = new ZipFile(zipPath)
|
|
||||||
def entries = input.entries().toList().sort { it.name }
|
|
||||||
|
|
||||||
def output = new ZipOutputStream(new FileOutputStream(destPath.toFile()))
|
|
||||||
output.setMethod(ZipOutputStream.DEFLATED)
|
|
||||||
|
|
||||||
entries.each {
|
|
||||||
def newEntry = new ZipEntry( it.name )
|
|
||||||
|
|
||||||
newEntry.setLastModifiedTime(zeroTime)
|
|
||||||
newEntry.setCreationTime(zeroTime)
|
|
||||||
newEntry.compressedSize = -1
|
|
||||||
newEntry.size = it.size
|
|
||||||
newEntry.crc = it.crc
|
|
||||||
|
|
||||||
output.putNextEntry(newEntry)
|
|
||||||
|
|
||||||
ByteStreams.copy(input.getInputStream(it), output)
|
|
||||||
|
|
||||||
output.closeEntry()
|
|
||||||
}
|
|
||||||
output.close()
|
|
||||||
input.close()
|
|
||||||
|
|
||||||
Files.move(destPath, Paths.get(zipPath), StandardCopyOption.REPLACE_EXISTING)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
|
|
||||||
apply plugin: CanonicalizerPlugin
|
apply plugin: CanonicalizerPlugin
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -11,6 +11,7 @@ package com.r3corda.contracts.isolated
|
|||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||||
|
@ -2,7 +2,7 @@ package com.r3corda.core.node
|
|||||||
|
|
||||||
import com.r3corda.core.contracts.ContractState
|
import com.r3corda.core.contracts.ContractState
|
||||||
import com.r3corda.core.contracts.PartyAndReference
|
import com.r3corda.core.contracts.PartyAndReference
|
||||||
import com.r3corda.core.contracts.TransactionBuilder
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
|
|
||||||
interface DummyContractBackdoor {
|
interface DummyContractBackdoor {
|
||||||
|
@ -3,19 +3,18 @@ package com.r3corda.contracts;
|
|||||||
import com.google.common.collect.*;
|
import com.google.common.collect.*;
|
||||||
import com.r3corda.contracts.asset.*;
|
import com.r3corda.contracts.asset.*;
|
||||||
import com.r3corda.core.contracts.*;
|
import com.r3corda.core.contracts.*;
|
||||||
import static com.r3corda.core.contracts.ContractsDSL.requireThat;
|
|
||||||
|
|
||||||
import com.r3corda.core.contracts.Timestamp;
|
import com.r3corda.core.contracts.Timestamp;
|
||||||
import com.r3corda.core.contracts.TransactionForContract.*;
|
import com.r3corda.core.contracts.TransactionForContract.*;
|
||||||
import com.r3corda.core.contracts.clauses.*;
|
import com.r3corda.core.contracts.clauses.*;
|
||||||
import com.r3corda.core.crypto.*;
|
import com.r3corda.core.crypto.*;
|
||||||
import kotlin.Unit;
|
import com.r3corda.core.transactions.*;
|
||||||
|
import kotlin.*;
|
||||||
import org.jetbrains.annotations.*;
|
import org.jetbrains.annotations.*;
|
||||||
|
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
import java.time.*;
|
import java.time.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.*;
|
||||||
|
|
||||||
import static com.r3corda.core.contracts.ContractsDSL.*;
|
import static com.r3corda.core.contracts.ContractsDSL.*;
|
||||||
import static kotlin.collections.CollectionsKt.*;
|
import static kotlin.collections.CollectionsKt.*;
|
||||||
@ -26,10 +25,9 @@ import static kotlin.collections.CollectionsKt.*;
|
|||||||
* use of Kotlin for implementation of the framework does not impose the same language choice on contract developers.
|
* use of Kotlin for implementation of the framework does not impose the same language choice on contract developers.
|
||||||
*/
|
*/
|
||||||
public class JavaCommercialPaper implements Contract {
|
public class JavaCommercialPaper implements Contract {
|
||||||
//public static SecureHash JCP_PROGRAM_ID = SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
|
|
||||||
private static final Contract JCP_PROGRAM_ID = new JavaCommercialPaper();
|
private static final Contract JCP_PROGRAM_ID = new JavaCommercialPaper();
|
||||||
|
|
||||||
public static class State implements ContractState, ICommercialPaperState {
|
public static class State implements OwnableState, ICommercialPaperState {
|
||||||
private PartyAndReference issuance;
|
private PartyAndReference issuance;
|
||||||
private PublicKey owner;
|
private PublicKey owner;
|
||||||
private Amount<Issued<Currency>> faceValue;
|
private Amount<Issued<Currency>> faceValue;
|
||||||
@ -54,6 +52,12 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
return new State(this.issuance, newOwner, this.faceValue, this.maturityDate);
|
return new State(this.issuance, newOwner, this.faceValue, this.maturityDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Pair<CommandData, OwnableState> withNewOwner(@NotNull PublicKey newOwner) {
|
||||||
|
return new Pair<>(new Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate));
|
||||||
|
}
|
||||||
|
|
||||||
public ICommercialPaperState withIssuance(PartyAndReference newIssuance) {
|
public ICommercialPaperState withIssuance(PartyAndReference newIssuance) {
|
||||||
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate);
|
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate);
|
||||||
}
|
}
|
||||||
@ -70,6 +74,7 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
return issuance;
|
return issuance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
public PublicKey getOwner() {
|
public PublicKey getOwner() {
|
||||||
return owner;
|
return owner;
|
||||||
}
|
}
|
||||||
@ -86,7 +91,6 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
@Override
|
@Override
|
||||||
public Contract getContract() {
|
public Contract getContract() {
|
||||||
return JCP_PROGRAM_ID;
|
return JCP_PROGRAM_ID;
|
||||||
//return SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -128,44 +132,17 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Clause {
|
public interface Clauses {
|
||||||
abstract class AbstractGroup implements GroupClause<State, State> {
|
class Group extends GroupClauseVerifier<State, Commands, State> {
|
||||||
@NotNull
|
// This complains because we're passing generic types into a varargs, but it is valid so we suppress the
|
||||||
@Override
|
// warning.
|
||||||
public MatchBehaviour getIfNotMatched() {
|
@SuppressWarnings("unchecked")
|
||||||
return MatchBehaviour.CONTINUE;
|
Group() {
|
||||||
}
|
super(new AnyComposition<>(
|
||||||
|
new Clauses.Redeem(),
|
||||||
@NotNull
|
new Clauses.Move(),
|
||||||
@Override
|
new Clauses.Issue()
|
||||||
public MatchBehaviour getIfMatched() {
|
));
|
||||||
return MatchBehaviour.END;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Group extends GroupClauseVerifier<State, State> {
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public MatchBehaviour getIfMatched() {
|
|
||||||
return MatchBehaviour.END;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public MatchBehaviour getIfNotMatched() {
|
|
||||||
return MatchBehaviour.ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public List<GroupClause<State, State>> getClauses() {
|
|
||||||
final List<GroupClause<State, State>> clauses = new ArrayList<>();
|
|
||||||
|
|
||||||
clauses.add(new Clause.Redeem());
|
|
||||||
clauses.add(new Clause.Move());
|
|
||||||
clauses.add(new Clause.Issue());
|
|
||||||
|
|
||||||
return clauses;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ -175,7 +152,7 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Move extends AbstractGroup {
|
class Move extends Clause<State, Commands, State> {
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||||
@ -184,11 +161,11 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||||
@NotNull List<? extends State> inputs,
|
@NotNull List<? extends State> inputs,
|
||||||
@NotNull List<? extends State> outputs,
|
@NotNull List<? extends State> outputs,
|
||||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||||
@NotNull State token) {
|
@NotNull State groupingKey) {
|
||||||
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
||||||
// There should be only a single input due to aggregation above
|
// There should be only a single input due to aggregation above
|
||||||
State input = single(inputs);
|
State input = single(inputs);
|
||||||
@ -206,7 +183,7 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Redeem extends AbstractGroup {
|
class Redeem extends Clause<State, Commands, State> {
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||||
@ -215,11 +192,11 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||||
@NotNull List<? extends State> inputs,
|
@NotNull List<? extends State> inputs,
|
||||||
@NotNull List<? extends State> outputs,
|
@NotNull List<? extends State> outputs,
|
||||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||||
@NotNull State token) {
|
@NotNull State groupingKey) {
|
||||||
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
|
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
|
||||||
|
|
||||||
// There should be only a single input due to aggregation above
|
// There should be only a single input due to aggregation above
|
||||||
@ -248,7 +225,7 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Issue extends AbstractGroup {
|
class Issue extends Clause<State, Commands, State> {
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||||
@ -257,14 +234,13 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||||
@NotNull List<? extends State> inputs,
|
@NotNull List<? extends State> inputs,
|
||||||
@NotNull List<? extends State> outputs,
|
@NotNull List<? extends State> outputs,
|
||||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||||
@NotNull State token) {
|
@NotNull State groupingKey) {
|
||||||
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
|
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
|
||||||
State output = single(outputs);
|
State output = single(outputs);
|
||||||
Party notary = cmd.getValue().notary;
|
|
||||||
Timestamp timestampCommand = tx.getTimestamp();
|
Timestamp timestampCommand = tx.getTimestamp();
|
||||||
Instant time = null == timestampCommand
|
Instant time = null == timestampCommand
|
||||||
? null
|
? null
|
||||||
@ -291,49 +267,28 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Redeem implements Commands {
|
class Redeem implements Commands {
|
||||||
private final Party notary;
|
|
||||||
|
|
||||||
public Redeem(Party setNotary) {
|
|
||||||
this.notary = setNotary;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) { return obj instanceof Redeem; }
|
public boolean equals(Object obj) { return obj instanceof Redeem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
class Issue implements Commands {
|
class Issue implements Commands {
|
||||||
private final Party notary;
|
|
||||||
|
|
||||||
public Issue(Party setNotary) {
|
|
||||||
this.notary = setNotary;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object obj) {
|
public boolean equals(Object obj) { return obj instanceof Issue; }
|
||||||
if (obj instanceof Issue) {
|
|
||||||
Issue other = (Issue)obj;
|
|
||||||
return notary.equals(other.notary);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() { return notary.hashCode(); }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
private List<AuthenticatedObject<Commands>> extractCommands(@NotNull TransactionForContract tx) {
|
||||||
return tx.getCommands()
|
return tx.getCommands()
|
||||||
.stream()
|
.stream()
|
||||||
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
|
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
|
||||||
|
.map((AuthenticatedObject<CommandData> command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException {
|
public void verify(@NotNull TransactionForContract tx) throws IllegalArgumentException {
|
||||||
ClauseVerifier.verifyClauses(tx, Collections.singletonList(new Clause.Group()), extractCommands(tx));
|
ClauseVerifier.verifyClause(tx, new Clauses.Group(), extractCommands(tx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ -346,13 +301,13 @@ public class JavaCommercialPaper implements Contract {
|
|||||||
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
|
public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount<Issued<Currency>> faceValue, @Nullable Instant maturityDate, @NotNull Party notary) {
|
||||||
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate);
|
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate);
|
||||||
TransactionState output = new TransactionState<>(state, notary);
|
TransactionState output = new TransactionState<>(state, notary);
|
||||||
return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(notary), issuance.getParty().getOwningKey()));
|
return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> wallet) throws InsufficientBalanceException {
|
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, List<StateAndRef<Cash.State>> wallet) throws InsufficientBalanceException {
|
||||||
new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), wallet, null);
|
new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), wallet, null);
|
||||||
tx.addInputState(paper);
|
tx.addInputState(paper);
|
||||||
tx.addCommand(new Command(new Commands.Redeem(paper.getState().getNotary()), paper.getState().getData().getOwner()));
|
tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, PublicKey newOwner) {
|
public void generateMove(TransactionBuilder tx, StateAndRef<State> paper, PublicKey newOwner) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.r3corda.contracts
|
package com.r3corda.contracts
|
||||||
|
|
||||||
import com.r3corda.contracts.asset.Cash
|
import com.r3corda.contracts.asset.Cash
|
||||||
|
import com.r3corda.contracts.asset.FungibleAsset
|
||||||
import com.r3corda.contracts.asset.InsufficientBalanceException
|
import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||||
import com.r3corda.contracts.asset.sumCashBy
|
import com.r3corda.contracts.asset.sumCashBy
|
||||||
import com.r3corda.contracts.clause.AbstractIssue
|
import com.r3corda.contracts.clause.AbstractIssue
|
||||||
@ -10,6 +11,7 @@ import com.r3corda.core.crypto.Party
|
|||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.crypto.toStringShort
|
import com.r3corda.core.crypto.toStringShort
|
||||||
import com.r3corda.core.random63BitValue
|
import com.r3corda.core.random63BitValue
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.utilities.Emoji
|
import com.r3corda.core.utilities.Emoji
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -49,10 +51,7 @@ class CommercialPaper : Contract {
|
|||||||
val maturityDate: Instant
|
val maturityDate: Instant
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
|
||||||
= tx.commands.select<Commands>()
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
|
|
||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
val issuance: PartyAndReference,
|
val issuance: PartyAndReference,
|
||||||
@ -79,25 +78,16 @@ class CommercialPaper : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Clauses {
|
interface Clauses {
|
||||||
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
|
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
|
||||||
override val ifNotMatched = MatchBehaviour.ERROR
|
AnyComposition(
|
||||||
override val ifMatched = MatchBehaviour.END
|
Redeem(),
|
||||||
override val clauses = listOf(
|
Move(),
|
||||||
Redeem(),
|
Issue())) {
|
||||||
Move(),
|
|
||||||
Issue()
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
|
class Issue : AbstractIssue<State, Commands, Terms>(
|
||||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
|
||||||
override val ifMatched = MatchBehaviour.END
|
|
||||||
}
|
|
||||||
|
|
||||||
class Issue : AbstractIssue<State, Terms>(
|
|
||||||
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
|
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
|
||||||
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
|
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
|
||||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||||
@ -105,10 +95,10 @@ class CommercialPaper : Contract {
|
|||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State>,
|
inputs: List<State>,
|
||||||
outputs: List<State>,
|
outputs: List<State>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: Issued<Terms>): Set<CommandData> {
|
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||||
val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
|
val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey)
|
||||||
val command = commands.requireSingleCommand<Commands.Issue>()
|
commands.requireSingleCommand<Commands.Issue>()
|
||||||
val timestamp = tx.timestamp
|
val timestamp = tx.timestamp
|
||||||
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||||
|
|
||||||
@ -118,14 +108,14 @@ class CommercialPaper : Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Move: AbstractGroupClause() {
|
class Move: Clause<State, Commands, Issued<Terms>>() {
|
||||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Move::class.java)
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State>,
|
inputs: List<State>,
|
||||||
outputs: List<State>,
|
outputs: List<State>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: Issued<Terms>): Set<CommandData> {
|
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||||
val command = commands.requireSingleCommand<Commands.Move>()
|
val command = commands.requireSingleCommand<Commands.Move>()
|
||||||
val input = inputs.single()
|
val input = inputs.single()
|
||||||
requireThat {
|
requireThat {
|
||||||
@ -138,15 +128,14 @@ class CommercialPaper : Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Redeem(): AbstractGroupClause() {
|
class Redeem(): Clause<State, Commands, Issued<Terms>>() {
|
||||||
override val requiredCommands: Set<Class<out CommandData>>
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
|
||||||
get() = setOf(Commands.Redeem::class.java)
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State>,
|
inputs: List<State>,
|
||||||
outputs: List<State>,
|
outputs: List<State>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: Issued<Terms>): Set<CommandData> {
|
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||||
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
|
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
|
||||||
// before requiring a single command
|
// before requiring a single command
|
||||||
val command = commands.requireSingleCommand<Commands.Redeem>()
|
val command = commands.requireSingleCommand<Commands.Redeem>()
|
||||||
@ -169,9 +158,9 @@ class CommercialPaper : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Commands : CommandData {
|
interface Commands : CommandData {
|
||||||
class Move : TypeOnlyCommandData(), Commands
|
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
||||||
data class Redeem(val notary: Party) : Commands
|
class Redeem : TypeOnlyCommandData(), Commands
|
||||||
data class Issue(val notary: Party, override val nonce: Long = random63BitValue()) : IssueCommand, Commands
|
data class Issue(override val nonce: Long = random63BitValue()) : IssueCommand, Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,7 +170,7 @@ class CommercialPaper : Contract {
|
|||||||
*/
|
*/
|
||||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant, notary: Party): TransactionBuilder {
|
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant, notary: Party): TransactionBuilder {
|
||||||
val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary)
|
val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary)
|
||||||
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(notary), issuance.party.owningKey))
|
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -206,7 +195,7 @@ class CommercialPaper : Contract {
|
|||||||
val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) }
|
val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) }
|
||||||
Cash().generateSpend(tx, amount, paper.state.data.owner, wallet)
|
Cash().generateSpend(tx, amount, paper.state.data.owner, wallet)
|
||||||
tx.addInputState(paper)
|
tx.addInputState(paper)
|
||||||
tx.addCommand(CommercialPaper.Commands.Redeem(paper.state.notary), paper.state.data.owner)
|
tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
package com.r3corda.contracts
|
package com.r3corda.contracts
|
||||||
|
|
||||||
|
import com.r3corda.contracts.asset.Cash
|
||||||
|
import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||||
import com.r3corda.contracts.asset.sumCashBy
|
import com.r3corda.contracts.asset.sumCashBy
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.crypto.NullPublicKey
|
import com.r3corda.core.crypto.NullPublicKey
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.crypto.toStringShort
|
import com.r3corda.core.crypto.toStringShort
|
||||||
|
import com.r3corda.core.node.services.Wallet
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.utilities.Emoji
|
import com.r3corda.core.utilities.Emoji
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -30,8 +34,7 @@ class CommercialPaperLegacy : Contract {
|
|||||||
val maturityDate: Instant
|
val maturityDate: Instant
|
||||||
) : OwnableState, ICommercialPaperState {
|
) : OwnableState, ICommercialPaperState {
|
||||||
override val contract = CP_LEGACY_PROGRAM_ID
|
override val contract = CP_LEGACY_PROGRAM_ID
|
||||||
override val participants: List<PublicKey>
|
override val participants = listOf(owner)
|
||||||
get() = listOf(owner)
|
|
||||||
|
|
||||||
fun withoutOwner() = copy(owner = NullPublicKey)
|
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||||
@ -46,11 +49,11 @@ class CommercialPaperLegacy : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Commands : CommandData {
|
interface Commands : CommandData {
|
||||||
class Move: TypeOnlyCommandData(), Commands
|
class Move : TypeOnlyCommandData(), Commands
|
||||||
data class Redeem(val notary: Party) : Commands
|
class Redeem : TypeOnlyCommandData(), Commands
|
||||||
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
|
// We don't need a nonce in the issue command, because the issuance.reference field should already be unique per CP.
|
||||||
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
|
// However, nothing in the platform enforces that uniqueness: it's up to the issuer.
|
||||||
data class Issue(val notary: Party) : Commands
|
class Issue : TypeOnlyCommandData(), Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract) {
|
override fun verify(tx: TransactionForContract) {
|
||||||
@ -74,8 +77,8 @@ class CommercialPaperLegacy : Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redemption of the paper requires movement of on-ledger cash.
|
|
||||||
is Commands.Redeem -> {
|
is Commands.Redeem -> {
|
||||||
|
// Redemption of the paper requires movement of on-ledger cash.
|
||||||
val input = inputs.single()
|
val input = inputs.single()
|
||||||
val received = tx.outputs.sumCashBy(input.owner)
|
val received = tx.outputs.sumCashBy(input.owner)
|
||||||
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||||
@ -97,6 +100,7 @@ class CommercialPaperLegacy : Contract {
|
|||||||
"output values sum to more than the inputs" by (output.faceValue.quantity > 0)
|
"output values sum to more than the inputs" by (output.faceValue.quantity > 0)
|
||||||
"the maturity date is not in the past" by (time < output.maturityDate)
|
"the maturity date is not in the past" by (time < output.maturityDate)
|
||||||
// Don't allow an existing CP state to be replaced by this issuance.
|
// Don't allow an existing CP state to be replaced by this issuance.
|
||||||
|
// TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version.
|
||||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||||
"output values sum to more than the inputs" by inputs.isEmpty()
|
"output values sum to more than the inputs" by inputs.isEmpty()
|
||||||
}
|
}
|
||||||
@ -107,4 +111,25 @@ class CommercialPaperLegacy : Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
|
||||||
|
notary: Party): TransactionBuilder {
|
||||||
|
val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate)
|
||||||
|
return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: PublicKey) {
|
||||||
|
tx.addInputState(paper)
|
||||||
|
tx.addOutputState(paper.state.data.withOwner(newOwner))
|
||||||
|
tx.addCommand(Command(Commands.Move(), paper.state.data.owner))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(InsufficientBalanceException::class)
|
||||||
|
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, wallet: Wallet) {
|
||||||
|
// Add the cash movement using the states in our wallet.
|
||||||
|
Cash().generateSpend(tx, paper.state.data.faceValue.withoutIssuer(),
|
||||||
|
paper.state.data.owner, wallet.statesOfType<Cash.State>())
|
||||||
|
tx.addInputState(paper)
|
||||||
|
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import com.r3corda.core.contracts.clauses.*
|
|||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow
|
import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow
|
||||||
import com.r3corda.protocols.TwoPartyDealProtocol
|
import com.r3corda.protocols.TwoPartyDealProtocol
|
||||||
import org.apache.commons.jexl3.JexlBuilder
|
import org.apache.commons.jexl3.JexlBuilder
|
||||||
@ -447,20 +448,14 @@ class InterestRateSwap() : Contract {
|
|||||||
fixingCalendar, index, indexSource, indexTenor)
|
fixingCalendar, index, indexSource, indexTenor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
|
override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
|
||||||
= tx.commands.select<Commands>()
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx))
|
interface Clauses {
|
||||||
|
|
||||||
interface Clause {
|
|
||||||
/**
|
/**
|
||||||
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
|
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
|
||||||
* helper functions for the clauses.
|
* helper functions for the clauses.
|
||||||
*/
|
*/
|
||||||
abstract class AbstractIRSClause : GroupClause<State, String> {
|
abstract class AbstractIRSClause : Clause<State, Commands, UniqueIdentifier>() {
|
||||||
override val ifMatched = MatchBehaviour.END
|
|
||||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
|
||||||
|
|
||||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||||
fun checkLegDates(legs: List<CommonLeg>) {
|
fun checkLegDates(legs: List<CommonLeg>) {
|
||||||
requireThat {
|
requireThat {
|
||||||
@ -502,23 +497,18 @@ class InterestRateSwap() : Contract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Group : GroupClauseVerifier<State, String>() {
|
class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
|
||||||
override val ifMatched = MatchBehaviour.END
|
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
|
||||||
override val ifNotMatched = MatchBehaviour.ERROR
|
|
||||||
|
|
||||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
|
|
||||||
// Group by Trade ID for in / out states
|
// Group by Trade ID for in / out states
|
||||||
= tx.groupStates() { state -> state.common.tradeID }
|
= tx.groupStates() { state -> state.linearId }
|
||||||
|
|
||||||
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Timestamped : SingleClause {
|
class Timestamped : Clause<ContractState, Commands, Unit>() {
|
||||||
override val ifMatched = MatchBehaviour.CONTINUE
|
override fun verify(tx: TransactionForContract,
|
||||||
override val ifNotMatched = MatchBehaviour.ERROR
|
inputs: List<ContractState>,
|
||||||
override val requiredCommands = emptySet<Class<out CommandData>>()
|
outputs: List<ContractState>,
|
||||||
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
groupingKey: Unit?): Set<Commands> {
|
||||||
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
|
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
|
||||||
// We return an empty set because we don't process any commands
|
// We return an empty set because we don't process any commands
|
||||||
return emptySet()
|
return emptySet()
|
||||||
@ -526,13 +516,13 @@ class InterestRateSwap() : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Agree : AbstractIRSClause() {
|
class Agree : AbstractIRSClause() {
|
||||||
override val requiredCommands = setOf(Commands.Agree::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Agree::class.java)
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State>,
|
inputs: List<State>,
|
||||||
outputs: List<State>,
|
outputs: List<State>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: String): Set<CommandData> {
|
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||||
val irs = outputs.filterIsInstance<State>().single()
|
val irs = outputs.filterIsInstance<State>().single()
|
||||||
requireThat {
|
requireThat {
|
||||||
@ -562,13 +552,13 @@ class InterestRateSwap() : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Fix : AbstractIRSClause() {
|
class Fix : AbstractIRSClause() {
|
||||||
override val requiredCommands = setOf(Commands.Refix::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Refix::class.java)
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State>,
|
inputs: List<State>,
|
||||||
outputs: List<State>,
|
outputs: List<State>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: String): Set<CommandData> {
|
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||||
val command = tx.commands.requireSingleCommand<Commands.Refix>()
|
val command = tx.commands.requireSingleCommand<Commands.Refix>()
|
||||||
val irs = outputs.filterIsInstance<State>().single()
|
val irs = outputs.filterIsInstance<State>().single()
|
||||||
val prevIrs = inputs.filterIsInstance<State>().single()
|
val prevIrs = inputs.filterIsInstance<State>().single()
|
||||||
@ -607,13 +597,13 @@ class InterestRateSwap() : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Pay : AbstractIRSClause() {
|
class Pay : AbstractIRSClause() {
|
||||||
override val requiredCommands = setOf(Commands.Pay::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Pay::class.java)
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State>,
|
inputs: List<State>,
|
||||||
outputs: List<State>,
|
outputs: List<State>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: String): Set<CommandData> {
|
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||||
val command = tx.commands.requireSingleCommand<Commands.Pay>()
|
val command = tx.commands.requireSingleCommand<Commands.Pay>()
|
||||||
requireThat {
|
requireThat {
|
||||||
"Payments not supported / verifiable yet" by false
|
"Payments not supported / verifiable yet" by false
|
||||||
@ -623,17 +613,18 @@ class InterestRateSwap() : Contract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Mature : AbstractIRSClause() {
|
class Mature : AbstractIRSClause() {
|
||||||
override val requiredCommands = setOf(Commands.Mature::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Mature::class.java)
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State>,
|
inputs: List<State>,
|
||||||
outputs: List<State>,
|
outputs: List<State>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: String): Set<CommandData> {
|
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||||
val command = tx.commands.requireSingleCommand<Commands.Mature>()
|
val command = tx.commands.requireSingleCommand<Commands.Mature>()
|
||||||
val irs = inputs.filterIsInstance<State>().single()
|
val irs = inputs.filterIsInstance<State>().single()
|
||||||
requireThat {
|
requireThat {
|
||||||
"No more fixings to be applied" by (irs.calculation.nextFixingDate() == null)
|
"No more fixings to be applied" by (irs.calculation.nextFixingDate() == null)
|
||||||
|
"The irs is fully consumed and there is no id matched output state" by outputs.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
return setOf(command.value)
|
return setOf(command.value)
|
||||||
@ -656,11 +647,12 @@ class InterestRateSwap() : Contract {
|
|||||||
val fixedLeg: FixedLeg,
|
val fixedLeg: FixedLeg,
|
||||||
val floatingLeg: FloatingLeg,
|
val floatingLeg: FloatingLeg,
|
||||||
val calculation: Calculation,
|
val calculation: Calculation,
|
||||||
val common: Common
|
val common: Common,
|
||||||
|
override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID)
|
||||||
) : FixableDealState, SchedulableState {
|
) : FixableDealState, SchedulableState {
|
||||||
|
|
||||||
override val contract = IRS_PROGRAM_ID
|
override val contract = IRS_PROGRAM_ID
|
||||||
override val thread = SecureHash.sha256(common.tradeID)
|
|
||||||
override val ref = common.tradeID
|
override val ref = common.tradeID
|
||||||
|
|
||||||
override val participants: List<PublicKey>
|
override val participants: List<PublicKey>
|
||||||
|
@ -4,10 +4,10 @@ import com.r3corda.contracts.clause.AbstractConserveAmount
|
|||||||
import com.r3corda.contracts.clause.AbstractIssue
|
import com.r3corda.contracts.clause.AbstractIssue
|
||||||
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
|
import com.r3corda.core.contracts.clauses.*
|
||||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
|
||||||
import com.r3corda.core.crypto.*
|
import com.r3corda.core.crypto.*
|
||||||
import com.r3corda.core.node.services.Wallet
|
import com.r3corda.core.node.services.Wallet
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.utilities.Emoji
|
import com.r3corda.core.utilities.Emoji
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -34,7 +34,7 @@ val CASH_PROGRAM_ID = Cash()
|
|||||||
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
|
* At the same time, other contracts that just want money and don't care much who is currently holding it in their
|
||||||
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
|
* vaults can ignore the issuer/depositRefs and just examine the amount fields.
|
||||||
*/
|
*/
|
||||||
class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* 1) hash should be of the contents, not the URI
|
* 1) hash should be of the contents, not the URI
|
||||||
@ -46,32 +46,30 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
|||||||
* that is inconsistent with the legal contract.
|
* that is inconsistent with the legal contract.
|
||||||
*/
|
*/
|
||||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
|
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
|
||||||
override val conserveClause: AbstractConserveAmount<State, Currency> = Clauses.ConserveAmount()
|
override val conserveClause: AbstractConserveAmount<State, Commands, Currency> = Clauses.ConserveAmount()
|
||||||
override val clauses = listOf(Clauses.Group())
|
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
|
||||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
= commands.select<Cash.Commands>()
|
||||||
= tx.commands.select<Cash.Commands>()
|
|
||||||
|
|
||||||
interface Clauses {
|
interface Clauses {
|
||||||
class Group : GroupClauseVerifier<State, Issued<Currency>>() {
|
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllComposition<State, Commands, Issued<Currency>>(
|
||||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
NoZeroSizedOutputs<State, Commands, Currency>(),
|
||||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
FirstComposition<State, Commands, Issued<Currency>>(
|
||||||
override val clauses = listOf(
|
|
||||||
NoZeroSizedOutputs<State, Currency>(),
|
|
||||||
Issue(),
|
Issue(),
|
||||||
ConserveAmount())
|
ConserveAmount())
|
||||||
|
)
|
||||||
|
) {
|
||||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
||||||
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
|
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
|
||||||
}
|
}
|
||||||
|
|
||||||
class Issue : AbstractIssue<State, Currency>(
|
class Issue : AbstractIssue<State, Commands, Currency>(
|
||||||
sum = { sumCash() },
|
sum = { sumCash() },
|
||||||
sumOrZero = { sumCashOrZero(it) }
|
sumOrZero = { sumCashOrZero(it) }
|
||||||
) {
|
) {
|
||||||
override val requiredCommands = setOf(Commands.Issue::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConserveAmount : AbstractConserveAmount<State, Currency>()
|
class ConserveAmount : AbstractConserveAmount<State, Commands, Currency>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A state representing a cash claim against some party. */
|
/** A state representing a cash claim against some party. */
|
||||||
@ -86,7 +84,7 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
|||||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||||
|
|
||||||
override val deposit = amount.token.issuer
|
override val deposit = amount.token.issuer
|
||||||
override val exitKeys = setOf(deposit.party.owningKey)
|
override val exitKeys = setOf(owner, deposit.party.owningKey)
|
||||||
override val contract = CASH_PROGRAM_ID
|
override val contract = CASH_PROGRAM_ID
|
||||||
override val issuanceDef = amount.token
|
override val issuanceDef = amount.token
|
||||||
override val participants = listOf(owner)
|
override val participants = listOf(owner)
|
||||||
@ -145,6 +143,9 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
|||||||
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
|
override fun generateExitCommand(amount: Amount<Issued<Currency>>) = Commands.Exit(amount)
|
||||||
override fun generateIssueCommand() = Commands.Issue()
|
override fun generateIssueCommand() = Commands.Issue()
|
||||||
override fun generateMoveCommand() = Commands.Move()
|
override fun generateMoveCommand() = Commands.Move()
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract)
|
||||||
|
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Small DSL extensions.
|
// Small DSL extensions.
|
||||||
|
@ -5,11 +5,13 @@ import com.r3corda.contracts.clause.AbstractIssue
|
|||||||
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
|
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
|
||||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
import com.r3corda.core.contracts.clauses.AnyComposition
|
||||||
|
import com.r3corda.core.contracts.clauses.verifyClause
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.crypto.newSecureRandom
|
import com.r3corda.core.crypto.newSecureRandom
|
||||||
import com.r3corda.core.crypto.toStringShort
|
import com.r3corda.core.crypto.toStringShort
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -28,9 +30,12 @@ val COMMODITY_PROGRAM_ID = CommodityContract()
|
|||||||
* differences are in representation of the underlying commodity. Issuer in this context means the party who has the
|
* differences are in representation of the underlying commodity. Issuer in this context means the party who has the
|
||||||
* commodity, or is otherwise responsible for delivering the commodity on demand, and the deposit reference is use for
|
* commodity, or is otherwise responsible for delivering the commodity on demand, and the deposit reference is use for
|
||||||
* internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse).
|
* internal accounting by the issuer (it might be, for example, a warehouse and/or location within a warehouse).
|
||||||
|
*
|
||||||
|
* This is an early stage example contract used to illustrate non-cash fungible assets, and is likely to change significantly
|
||||||
|
* in future.
|
||||||
*/
|
*/
|
||||||
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
|
// TODO: Need to think about expiry of commodities, how to require payment of storage costs, etc.
|
||||||
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, CommodityContract.State>() {
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* 1) hash should be of the contents, not the URI
|
* 1) hash should be of the contents, not the URI
|
||||||
@ -43,7 +48,7 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
|||||||
*/
|
*/
|
||||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/commodity-claims.html")
|
||||||
|
|
||||||
override val conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount()
|
override val conserveClause: AbstractConserveAmount<State, Commands, Commodity> = Clauses.ConserveAmount()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The clauses for this contract are essentially:
|
* The clauses for this contract are essentially:
|
||||||
@ -59,24 +64,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
|||||||
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over
|
* Grouping clause to extract input and output states into matched groups and then run a set of clauses over
|
||||||
* each group.
|
* each group.
|
||||||
*/
|
*/
|
||||||
class Group : GroupClauseVerifier<State, Issued<Commodity>>() {
|
class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyComposition(
|
||||||
/**
|
NoZeroSizedOutputs<State, Commands, Commodity>(),
|
||||||
* The group clause does not depend on any commands being present, so something has gone terribly wrong if
|
Issue(),
|
||||||
* it doesn't match.
|
ConserveAmount())) {
|
||||||
*/
|
|
||||||
override val ifNotMatched = MatchBehaviour.ERROR
|
|
||||||
/**
|
|
||||||
* The group clause is the only top level clause, so end after processing it. If there are any commands left
|
|
||||||
* after this clause has run, the clause verifier will trigger an error.
|
|
||||||
*/
|
|
||||||
override val ifMatched = MatchBehaviour.END
|
|
||||||
// Subclauses to run on each group
|
|
||||||
override val clauses = listOf(
|
|
||||||
NoZeroSizedOutputs<State, Commodity>(),
|
|
||||||
Issue(),
|
|
||||||
ConserveAmount()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group commodity states by issuance definition (issuer and underlying commodity).
|
* Group commodity states by issuance definition (issuer and underlying commodity).
|
||||||
*/
|
*/
|
||||||
@ -87,17 +78,17 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
|||||||
/**
|
/**
|
||||||
* Standard issue clause, specialised to match the commodity issue command.
|
* Standard issue clause, specialised to match the commodity issue command.
|
||||||
*/
|
*/
|
||||||
class Issue : AbstractIssue<State, Commodity>(
|
class Issue : AbstractIssue<State, Commands, Commodity>(
|
||||||
sum = { sumCommodities() },
|
sum = { sumCommodities() },
|
||||||
sumOrZero = { sumCommoditiesOrZero(it) }
|
sumOrZero = { sumCommoditiesOrZero(it) }
|
||||||
) {
|
) {
|
||||||
override val requiredCommands = setOf(Commands.Issue::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard clause for conserving the amount from input to output.
|
* Standard clause for conserving the amount from input to output.
|
||||||
*/
|
*/
|
||||||
class ConserveAmount : AbstractConserveAmount<State, Commodity>()
|
class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A state representing a commodity claim against some party */
|
/** A state representing a commodity claim against some party */
|
||||||
@ -147,9 +138,10 @@ class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
|||||||
*/
|
*/
|
||||||
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
|
data class Exit(override val amount: Amount<Issued<Commodity>>) : Commands, FungibleAsset.Commands.Exit<Commodity>
|
||||||
}
|
}
|
||||||
override val clauses = listOf(Clauses.Group())
|
override fun verify(tx: TransactionForContract)
|
||||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||||
= tx.commands.select<CommodityContract.Commands>()
|
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Commands>>
|
||||||
|
= commands.select<CommodityContract.Commands>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
|
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
|
||||||
|
@ -29,7 +29,10 @@ interface FungibleAsset<T> : OwnableState {
|
|||||||
val deposit: PartyAndReference
|
val deposit: PartyAndReference
|
||||||
val issuanceDef: Issued<T>
|
val issuanceDef: Issued<T>
|
||||||
val amount: Amount<Issued<T>>
|
val amount: Amount<Issued<T>>
|
||||||
/** There must be an ExitCommand signed by these keys to destroy the amount */
|
/**
|
||||||
|
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
|
||||||
|
* owner to sign, some (i.e. cash) also require the issuer.
|
||||||
|
*/
|
||||||
val exitKeys: Collection<PublicKey>
|
val exitKeys: Collection<PublicKey>
|
||||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||||
override val owner: PublicKey
|
override val owner: PublicKey
|
||||||
|
@ -4,16 +4,11 @@ import com.google.common.annotations.VisibleForTesting
|
|||||||
import com.r3corda.contracts.clause.*
|
import com.r3corda.contracts.clause.*
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.*
|
import com.r3corda.core.contracts.clauses.*
|
||||||
import com.r3corda.core.crypto.NullPublicKey
|
import com.r3corda.core.crypto.*
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.crypto.SecureHash
|
|
||||||
import com.r3corda.core.crypto.toStringShort
|
|
||||||
import com.r3corda.core.random63BitValue
|
import com.r3corda.core.random63BitValue
|
||||||
import com.r3corda.core.testing.MINI_CORP
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.testing.TEST_TX_TIME
|
import com.r3corda.core.utilities.*
|
||||||
import com.r3corda.core.utilities.Emoji
|
import java.math.BigInteger
|
||||||
import com.r3corda.core.utilities.NonEmptySet
|
|
||||||
import com.r3corda.core.utilities.nonEmptySetOf
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -43,25 +38,27 @@ class Obligation<P> : Contract {
|
|||||||
* that is inconsistent with the legal contract.
|
* that is inconsistent with the legal contract.
|
||||||
*/
|
*/
|
||||||
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
|
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.example.gov/cash-settlement.html")
|
||||||
private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
|
|
||||||
Clauses.Group<P>())
|
|
||||||
|
|
||||||
interface Clauses {
|
interface Clauses {
|
||||||
/**
|
/**
|
||||||
* Parent clause for clauses that operate on grouped states (those which are fungible).
|
* Parent clause for clauses that operate on grouped states (those which are fungible).
|
||||||
*/
|
*/
|
||||||
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() {
|
class Group<P> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
|
||||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
AllComposition(
|
||||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
|
||||||
override val clauses = listOf(
|
FirstComposition(
|
||||||
NoZeroSizedOutputs<State<P>, Terms<P>>(),
|
SetLifecycle<P>(),
|
||||||
SetLifecycle<P>(),
|
AllComposition(
|
||||||
VerifyLifecycle<P>(),
|
VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
|
||||||
Settle<P>(),
|
FirstComposition(
|
||||||
Issue(),
|
Settle<P>(),
|
||||||
ConserveAmount()
|
Issue(),
|
||||||
)
|
ConserveAmount()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
||||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
|
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
|
||||||
}
|
}
|
||||||
@ -69,58 +66,64 @@ class Obligation<P> : Contract {
|
|||||||
/**
|
/**
|
||||||
* Generic issuance clause
|
* Generic issuance clause
|
||||||
*/
|
*/
|
||||||
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
class Issue<P> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
||||||
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic move/exit clause for fungible assets
|
* Generic move/exit clause for fungible assets
|
||||||
*/
|
*/
|
||||||
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Terms<P>>()
|
class ConserveAmount<P> : AbstractConserveAmount<State<P>, Commands, Terms<P>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clause for supporting netting of obligations.
|
* Clause for supporting netting of obligations.
|
||||||
*/
|
*/
|
||||||
class Net<P> : NetClause<P>()
|
class Net<C: CommandData, P> : NetClause<C, P>() {
|
||||||
|
val lifecycleClause = Clauses.VerifyLifecycle<ContractState, C, Unit, P>()
|
||||||
|
override fun toString(): String = "Net obligations"
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract, inputs: List<ContractState>, outputs: List<ContractState>, commands: List<AuthenticatedObject<C>>, groupingKey: Unit?): Set<C> {
|
||||||
|
lifecycleClause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||||
|
return super.verify(tx, inputs, outputs, commands, groupingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obligation-specific clause for changing the lifecycle of one or more states.
|
* Obligation-specific clause for changing the lifecycle of one or more states.
|
||||||
*/
|
*/
|
||||||
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
class SetLifecycle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||||
override val requiredCommands = setOf(Commands.SetLifecycle::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
|
||||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
|
||||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State<P>>,
|
inputs: List<State<P>>,
|
||||||
outputs: List<State<P>>,
|
outputs: List<State<P>>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: Issued<Terms<P>>): Set<CommandData> {
|
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||||
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
|
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
|
||||||
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
|
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
|
||||||
return setOf(command.value)
|
return setOf(command.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "Set obligation lifecycle"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obligation-specific clause for settling an outstanding obligation by witnessing
|
* Obligation-specific clause for settling an outstanding obligation by witnessing
|
||||||
* change of ownership of other states to fulfil
|
* change of ownership of other states to fulfil
|
||||||
*/
|
*/
|
||||||
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
class Settle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||||
override val requiredCommands = setOf(Commands.Settle::class.java)
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
|
||||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
|
||||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State<P>>,
|
inputs: List<State<P>>,
|
||||||
outputs: List<State<P>>,
|
outputs: List<State<P>>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<Commands>>,
|
||||||
token: Issued<Terms<P>>): Set<CommandData> {
|
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||||
|
require(groupingKey != null)
|
||||||
val command = commands.requireSingleCommand<Commands.Settle<P>>()
|
val command = commands.requireSingleCommand<Commands.Settle<P>>()
|
||||||
val obligor = token.issuer.party
|
val obligor = groupingKey!!.issuer.party
|
||||||
val template = token.product
|
val template = groupingKey.product
|
||||||
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
val inputAmount: Amount<Issued<Terms<P>>> = inputs.sumObligationsOrNull<P>() ?: throw IllegalArgumentException("there is at least one obligation input for this group")
|
||||||
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(token)
|
val outputAmount: Amount<Issued<Terms<P>>> = outputs.sumObligationsOrZero(groupingKey)
|
||||||
|
|
||||||
// Sum up all asset state objects that are moving and fulfil our requirements
|
// Sum up all asset state objects that are moving and fulfil our requirements
|
||||||
|
|
||||||
@ -166,7 +169,7 @@ class Obligation<P> : Contract {
|
|||||||
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
||||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||||
if (settled != null) {
|
if (settled != null) {
|
||||||
val debt = obligations.sumObligationsOrZero(token)
|
val debt = obligations.sumObligationsOrZero(groupingKey)
|
||||||
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
require(settled.quantity <= debt.quantity) { "Payment of $settled must not exceed debt $debt" }
|
||||||
totalPenniesSettled += settled.quantity
|
totalPenniesSettled += settled.quantity
|
||||||
}
|
}
|
||||||
@ -185,7 +188,7 @@ class Obligation<P> : Contract {
|
|||||||
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
|
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
|
||||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||||
"at obligor ${obligor.name} the obligations after settlement balance" by
|
"at obligor ${obligor.name} the obligations after settlement balance" by
|
||||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, token))
|
(inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
|
||||||
}
|
}
|
||||||
return setOf(command.value)
|
return setOf(command.value)
|
||||||
}
|
}
|
||||||
@ -197,26 +200,15 @@ class Obligation<P> : Contract {
|
|||||||
* any lifecycle change clause, which is the only clause that involve
|
* any lifecycle change clause, which is the only clause that involve
|
||||||
* non-standard lifecycle states on input/output.
|
* non-standard lifecycle states on input/output.
|
||||||
*/
|
*/
|
||||||
class VerifyLifecycle<P> : SingleClause, GroupClause<State<P>, Issued<Terms<P>>> {
|
class VerifyLifecycle<S: ContractState, C: CommandData, T: Any, P> : Clause<S, C, T>() {
|
||||||
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
|
||||||
override val ifMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
|
||||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
|
||||||
= verify(
|
|
||||||
tx.inputs.filterIsInstance<State<P>>(),
|
|
||||||
tx.outputs.filterIsInstance<State<P>>()
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<State<P>>,
|
inputs: List<S>,
|
||||||
outputs: List<State<P>>,
|
outputs: List<S>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<C>>,
|
||||||
token: Issued<Terms<P>>): Set<CommandData>
|
groupingKey: T?): Set<C>
|
||||||
= verify(inputs, outputs)
|
= verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
|
||||||
|
private fun verify(inputs: List<State<P>>,
|
||||||
fun verify(inputs: List<State<P>>,
|
outputs: List<State<P>>): Set<C> {
|
||||||
outputs: List<State<P>>): Set<CommandData> {
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
|
"all inputs are in the normal state " by inputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||||
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
"all outputs are in the normal state " by outputs.all { it.lifecycle == Lifecycle.NORMAL }
|
||||||
@ -334,7 +326,7 @@ class Obligation<P> : Contract {
|
|||||||
* Net two or more obligation states together in a close-out netting style. Limited to bilateral netting
|
* Net two or more obligation states together in a close-out netting style. Limited to bilateral netting
|
||||||
* as only the beneficiary (not the obligor) needs to sign.
|
* as only the beneficiary (not the obligor) needs to sign.
|
||||||
*/
|
*/
|
||||||
data class Net(val type: NetType) : Obligation.Commands
|
data class Net(override val type: NetType) : NetCommand, Commands
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command stating that a debt has been moved, optionally to fulfil another contract.
|
* A command stating that a debt has been moved, optionally to fulfil another contract.
|
||||||
@ -377,9 +369,10 @@ class Obligation<P> : Contract {
|
|||||||
data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
|
data class Exit<P>(override val amount: Amount<Issued<Terms<P>>>) : Commands, FungibleAsset.Commands.Exit<Terms<P>>
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
override fun verify(tx: TransactionForContract) = verifyClause<Commands>(tx, FirstComposition<ContractState, Commands, Unit>(
|
||||||
= tx.commands.select<Obligation.Commands>()
|
Clauses.Net<Commands, P>(),
|
||||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
|
Clauses.Group<P>()
|
||||||
|
), tx.commands.select<Obligation.Commands>())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
* A default command mutates inputs and produces identical outputs, except that the lifecycle changes.
|
||||||
@ -451,17 +444,16 @@ class Obligation<P> : Contract {
|
|||||||
*
|
*
|
||||||
* @param tx transaction builder to add states and commands to.
|
* @param tx transaction builder to add states and commands to.
|
||||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||||
* @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
|
||||||
* necessarily owned by us.
|
|
||||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||||
* the responsibility of the caller to check that they do not exit funds held by others.
|
* the responsibility of the caller to check that they do not exit funds held by others.
|
||||||
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
|
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
|
||||||
changeKey: PublicKey, assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
|
assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
|
||||||
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, changeKey, assetStates,
|
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, assetStates,
|
||||||
deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) },
|
deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) },
|
||||||
|
generateMoveCommand = { -> Commands.Move() },
|
||||||
generateExitCommand = { amount -> Commands.Exit(amount) }
|
generateExitCommand = { amount -> Commands.Exit(amount) }
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -718,7 +710,12 @@ infix fun <T> Obligation.State<T>.`issued by`(party: Party) = copy(obligor = par
|
|||||||
@Suppress("unused") fun <T> Obligation.State<T>.ownedBy(owner: PublicKey) = copy(beneficiary = owner)
|
@Suppress("unused") fun <T> Obligation.State<T>.ownedBy(owner: PublicKey) = copy(beneficiary = owner)
|
||||||
@Suppress("unused") fun <T> Obligation.State<T>.issuedBy(party: Party) = copy(obligor = party)
|
@Suppress("unused") fun <T> Obligation.State<T>.issuedBy(party: Party) = copy(obligor = party)
|
||||||
|
|
||||||
|
/** A randomly generated key. */
|
||||||
|
val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) }
|
||||||
|
/** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */
|
||||||
|
val DUMMY_OBLIGATION_ISSUER by lazy { Party("Snake Oil Issuer", DUMMY_OBLIGATION_ISSUER_KEY.public) }
|
||||||
|
|
||||||
val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency>
|
val Issued<Currency>.OBLIGATION_DEF: Obligation.Terms<Currency>
|
||||||
get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME)
|
get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME)
|
||||||
val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>
|
val Amount<Issued<Currency>>.OBLIGATION: Obligation.State<Currency>
|
||||||
get() = Obligation.State(Obligation.Lifecycle.NORMAL, MINI_CORP, token.OBLIGATION_DEF, quantity, NullPublicKey)
|
get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NullPublicKey)
|
||||||
|
@ -2,9 +2,8 @@ package com.r3corda.contracts.asset
|
|||||||
|
|
||||||
import com.r3corda.contracts.clause.AbstractConserveAmount
|
import com.r3corda.contracts.clause.AbstractConserveAmount
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.SingleClause
|
|
||||||
import com.r3corda.core.contracts.clauses.verifyClauses
|
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -25,12 +24,9 @@ import java.security.PublicKey
|
|||||||
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
|
* At the same time, other contracts that just want assets and don't care much who is currently holding it can ignore
|
||||||
* the issuer/depositRefs and just examine the amount fields.
|
* the issuer/depositRefs and just examine the amount fields.
|
||||||
*/
|
*/
|
||||||
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract {
|
abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Contract {
|
||||||
abstract val clauses: List<SingleClause>
|
abstract fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
|
||||||
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
|
abstract val conserveClause: AbstractConserveAmount<S, C, T>
|
||||||
abstract val conserveClause: AbstractConserveAmount<S, T>
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate an transaction exiting assets from the ledger.
|
* Generate an transaction exiting assets from the ledger.
|
||||||
@ -44,9 +40,10 @@ abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract {
|
|||||||
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
||||||
*/
|
*/
|
||||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||||
changeKey: PublicKey, assetStates: List<StateAndRef<S>>): PublicKey
|
assetStates: List<StateAndRef<S>>): PublicKey
|
||||||
= conserveClause.generateExit(tx, amountIssued, changeKey, assetStates,
|
= conserveClause.generateExit(tx, amountIssued, assetStates,
|
||||||
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
|
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
|
||||||
|
generateMoveCommand = { -> generateMoveCommand() },
|
||||||
generateExitCommand = { amount -> generateExitCommand(amount) }
|
generateExitCommand = { amount -> generateExitCommand(amount) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,9 +5,9 @@ import com.r3corda.contracts.asset.InsufficientBalanceException
|
|||||||
import com.r3corda.contracts.asset.sumFungibleOrNull
|
import com.r3corda.contracts.asset.sumFungibleOrNull
|
||||||
import com.r3corda.contracts.asset.sumFungibleOrZero
|
import com.r3corda.contracts.asset.sumFungibleOrZero
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.GroupClause
|
import com.r3corda.core.contracts.clauses.Clause
|
||||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -16,14 +16,7 @@ import java.util.*
|
|||||||
* Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
|
* Move command is provided, and errors if absent. Must be the last clause under a grouping clause;
|
||||||
* errors on no-match, ends on match.
|
* errors on no-match, ends on match.
|
||||||
*/
|
*/
|
||||||
abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
|
abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
|
||||||
override val ifMatched: MatchBehaviour
|
|
||||||
get() = MatchBehaviour.END
|
|
||||||
override val ifNotMatched: MatchBehaviour
|
|
||||||
get() = MatchBehaviour.ERROR
|
|
||||||
override val requiredCommands: Set<Class<out CommandData>>
|
|
||||||
get() = emptySet()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
|
* Gather assets from the given list of states, sufficient to match or exceed the given amount.
|
||||||
*
|
*
|
||||||
@ -53,16 +46,16 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
|
|||||||
*
|
*
|
||||||
* @param tx transaction builder to add states and commands to.
|
* @param tx transaction builder to add states and commands to.
|
||||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||||
* @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
|
||||||
* necessarily owned by us.
|
|
||||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||||
* the responsibility of the caller to check that they do not exit funds held by others.
|
* the responsibility of the caller to check that they do not attempt to exit funds held by others.
|
||||||
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
* @return the public key of the assets issuer, who must sign the transaction for it to be valid.
|
||||||
*/
|
*/
|
||||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||||
changeKey: PublicKey, assetStates: List<StateAndRef<S>>,
|
assetStates: List<StateAndRef<S>>,
|
||||||
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
|
deriveState: (TransactionState<S>, Amount<Issued<T>>, PublicKey) -> TransactionState<S>,
|
||||||
|
generateMoveCommand: () -> CommandData,
|
||||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey {
|
generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey {
|
||||||
|
val owner = assetStates.map { it.state.data.owner }.toSet().single()
|
||||||
val currency = amountIssued.token.product
|
val currency = amountIssued.token.product
|
||||||
val amount = Amount(amountIssued.quantity, currency)
|
val amount = Amount(amountIssued.quantity, currency)
|
||||||
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
|
var acceptableCoins = assetStates.filter { ref -> ref.state.data.amount.token == amountIssued.token }
|
||||||
@ -82,12 +75,13 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
|
|||||||
|
|
||||||
val outputs = if (change != null) {
|
val outputs = if (change != null) {
|
||||||
// Add a change output and adjust the last output downwards.
|
// Add a change output and adjust the last output downwards.
|
||||||
listOf(deriveState(gathered.last().state, change, changeKey))
|
listOf(deriveState(gathered.last().state, change, owner))
|
||||||
} else emptyList()
|
} else emptyList()
|
||||||
|
|
||||||
for (state in gathered) tx.addInputState(state)
|
for (state in gathered) tx.addInputState(state)
|
||||||
for (state in outputs) tx.addOutputState(state)
|
for (state in outputs) tx.addOutputState(state)
|
||||||
tx.addCommand(generateExitCommand(amountIssued), amountIssued.token.issuer.party.owningKey)
|
tx.addCommand(generateMoveCommand(), gathered.map { it.state.data.owner })
|
||||||
|
tx.addCommand(generateExitCommand(amountIssued), gathered.flatMap { it.state.data.exitKeys })
|
||||||
return amountIssued.token.issuer.party.owningKey
|
return amountIssued.token.issuer.party.owningKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,26 +172,31 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
|
|||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<S>,
|
inputs: List<S>,
|
||||||
outputs: List<S>,
|
outputs: List<S>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<C>>,
|
||||||
token: Issued<T>): Set<CommandData> {
|
groupingKey: Issued<T>?): Set<C> {
|
||||||
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $token")
|
require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" }
|
||||||
val deposit = token.issuer
|
val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> }
|
||||||
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(token)
|
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $groupingKey")
|
||||||
|
val deposit = groupingKey!!.issuer
|
||||||
|
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(groupingKey)
|
||||||
|
|
||||||
// If we want to remove assets from the ledger, that must be signed for by the issuer.
|
// If we want to remove assets from the ledger, that must be signed for by the issuer and owner.
|
||||||
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
|
|
||||||
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
val exitKeys: Set<PublicKey> = inputs.flatMap { it.exitKeys }.toSet()
|
||||||
val exitCommand = tx.commands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter {it.value.amount.token == token}.singleOrNull()
|
val exitCommand = matchedCommands.select<FungibleAsset.Commands.Exit<T>>(parties = null, signers = exitKeys).filter { it.value.amount.token == groupingKey }.singleOrNull()
|
||||||
val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, token)
|
val amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, groupingKey)
|
||||||
|
|
||||||
requireThat {
|
requireThat {
|
||||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||||
"for reference ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
|
"for reference ${deposit.reference} at issuer ${deposit.party.name} the amounts balance: ${inputAmount.quantity} - ${amountExitingLedger.quantity} != ${outputAmount.quantity}" by
|
||||||
(inputAmount == outputAmount + amountExitingLedger)
|
(inputAmount == outputAmount + amountExitingLedger)
|
||||||
}
|
}
|
||||||
|
|
||||||
return listOf(exitCommand?.value, verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, tx))
|
verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, commands)
|
||||||
.filter { it != null }
|
|
||||||
.requireNoNulls().toSet()
|
// This is safe because we've taken the commands from a collection of C objects at the start
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return matchedCommands.map { it.value }.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "Conserve amount between inputs and outputs"
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
package com.r3corda.contracts.clause
|
package com.r3corda.contracts.clause
|
||||||
|
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.GroupClause
|
import com.r3corda.core.contracts.clauses.Clause
|
||||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard issue clause for contracts that issue fungible assets.
|
* Standard issue clause for contracts that issue fungible assets.
|
||||||
@ -14,20 +13,16 @@ import com.r3corda.core.contracts.clauses.MatchBehaviour
|
|||||||
* @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are
|
* @param sumOrZero function to convert a list of states into an amount of the token, and returns zero if there are
|
||||||
* no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed.
|
* no states in the list. Takes in an instance of the token definition for constructing the zero amount if needed.
|
||||||
*/
|
*/
|
||||||
abstract class AbstractIssue<in S: ContractState, T: Any>(
|
abstract class AbstractIssue<in S: ContractState, C: CommandData, T: Any>(
|
||||||
val sum: List<S>.() -> Amount<Issued<T>>,
|
val sum: List<S>.() -> Amount<Issued<T>>,
|
||||||
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
|
val sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
|
||||||
) : GroupClause<S, Issued<T>> {
|
) : Clause<S, C, Issued<T>>() {
|
||||||
override val ifMatched: MatchBehaviour
|
|
||||||
get() = MatchBehaviour.END
|
|
||||||
override val ifNotMatched: MatchBehaviour
|
|
||||||
get() = MatchBehaviour.CONTINUE
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<S>,
|
inputs: List<S>,
|
||||||
outputs: List<S>,
|
outputs: List<S>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<C>>,
|
||||||
token: Issued<T>): Set<CommandData> {
|
groupingKey: Issued<T>?): Set<C> {
|
||||||
|
require(groupingKey != null)
|
||||||
// TODO: Take in matched commands as a parameter
|
// TODO: Take in matched commands as a parameter
|
||||||
val issueCommand = commands.requireSingleCommand<IssueCommand>()
|
val issueCommand = commands.requireSingleCommand<IssueCommand>()
|
||||||
|
|
||||||
@ -42,8 +37,8 @@ abstract class AbstractIssue<in S: ContractState, T: Any>(
|
|||||||
// external mechanism (such as locally defined rules on which parties are trustworthy).
|
// external mechanism (such as locally defined rules on which parties are trustworthy).
|
||||||
|
|
||||||
// The grouping already ensures that all outputs have the same deposit reference and token.
|
// The grouping already ensures that all outputs have the same deposit reference and token.
|
||||||
val issuer = token.issuer.party
|
val issuer = groupingKey!!.issuer.party
|
||||||
val inputAmount = inputs.sumOrZero(token)
|
val inputAmount = inputs.sumOrZero(groupingKey)
|
||||||
val outputAmount = outputs.sum()
|
val outputAmount = outputs.sum()
|
||||||
requireThat {
|
requireThat {
|
||||||
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
|
||||||
@ -53,6 +48,8 @@ abstract class AbstractIssue<in S: ContractState, T: Any>(
|
|||||||
"output values sum to more than the inputs" by (outputAmount > inputAmount)
|
"output values sum to more than the inputs" by (outputAmount > inputAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return setOf(issueCommand.value)
|
// This is safe because we've taken the command from a collection of C objects at the start
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return setOf(issueCommand.value as C)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,7 @@ import com.r3corda.contracts.asset.Obligation
|
|||||||
import com.r3corda.contracts.asset.extractAmountsDue
|
import com.r3corda.contracts.asset.extractAmountsDue
|
||||||
import com.r3corda.contracts.asset.sumAmountsDue
|
import com.r3corda.contracts.asset.sumAmountsDue
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
import com.r3corda.core.contracts.clauses.Clause
|
||||||
import com.r3corda.core.contracts.clauses.SingleClause
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,25 +42,25 @@ data class MultilateralNetState<P>(
|
|||||||
* Clause for netting contract states. Currently only supports obligation contract.
|
* Clause for netting contract states. Currently only supports obligation contract.
|
||||||
*/
|
*/
|
||||||
// TODO: Make this usable for any nettable contract states
|
// TODO: Make this usable for any nettable contract states
|
||||||
open class NetClause<P> : SingleClause {
|
open class NetClause<C: CommandData, P> : Clause<ContractState, C, Unit>() {
|
||||||
override val ifNotMatched: MatchBehaviour
|
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
|
||||||
get() = MatchBehaviour.CONTINUE
|
|
||||||
override val ifMatched: MatchBehaviour
|
|
||||||
get() = MatchBehaviour.END
|
|
||||||
override val requiredCommands: Set<Class<out CommandData>>
|
|
||||||
get() = setOf(Obligation.Commands.Net::class.java)
|
|
||||||
|
|
||||||
@Suppress("ConvertLambdaToReference")
|
@Suppress("ConvertLambdaToReference")
|
||||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
override fun verify(tx: TransactionForContract,
|
||||||
val command = commands.requireSingleCommand<Obligation.Commands.Net>()
|
inputs: List<ContractState>,
|
||||||
|
outputs: List<ContractState>,
|
||||||
|
commands: List<AuthenticatedObject<C>>,
|
||||||
|
groupingKey: Unit?): Set<C> {
|
||||||
|
val matchedCommands: List<AuthenticatedObject<C>> = commands.filter { it.value is NetCommand }
|
||||||
|
val command = matchedCommands.requireSingleCommand<Obligation.Commands.Net>()
|
||||||
val groups = when (command.value.type) {
|
val groups = when (command.value.type) {
|
||||||
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
|
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
|
||||||
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
|
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
|
||||||
}
|
}
|
||||||
for ((inputs, outputs, key) in groups) {
|
for ((groupInputs, groupOutputs, key) in groups) {
|
||||||
verifyNetCommand(inputs, outputs, command, key)
|
verifyNetCommand(groupInputs, groupOutputs, command, key)
|
||||||
}
|
}
|
||||||
return setOf(command.value)
|
return matchedCommands.map { it.value }.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -70,7 +69,7 @@ open class NetClause<P> : SingleClause {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun verifyNetCommand(inputs: List<Obligation.State<P>>,
|
fun verifyNetCommand(inputs: List<Obligation.State<P>>,
|
||||||
outputs: List<Obligation.State<P>>,
|
outputs: List<Obligation.State<P>>,
|
||||||
command: AuthenticatedObject<Obligation.Commands.Net>,
|
command: AuthenticatedObject<NetCommand>,
|
||||||
netState: NetState<P>) {
|
netState: NetState<P>) {
|
||||||
val template = netState.template
|
val template = netState.template
|
||||||
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
|
// Create two maps of balances from obligors to beneficiaries, one for input states, the other for output states.
|
||||||
|
@ -2,29 +2,23 @@ package com.r3corda.contracts.clause
|
|||||||
|
|
||||||
import com.r3corda.contracts.asset.FungibleAsset
|
import com.r3corda.contracts.asset.FungibleAsset
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.contracts.clauses.GroupClause
|
import com.r3corda.core.contracts.clauses.Clause
|
||||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clause for fungible asset contracts, which enforces that no output state should have
|
* Clause for fungible asset contracts, which enforces that no output state should have
|
||||||
* a balance of zero.
|
* a balance of zero.
|
||||||
*/
|
*/
|
||||||
open class NoZeroSizedOutputs<in S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
|
open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
|
||||||
override val ifMatched: MatchBehaviour
|
|
||||||
get() = MatchBehaviour.CONTINUE
|
|
||||||
override val ifNotMatched: MatchBehaviour
|
|
||||||
get() = MatchBehaviour.ERROR
|
|
||||||
override val requiredCommands: Set<Class<CommandData>>
|
|
||||||
get() = emptySet()
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<S>,
|
inputs: List<S>,
|
||||||
outputs: List<S>,
|
outputs: List<S>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
commands: List<AuthenticatedObject<C>>,
|
||||||
token: Issued<T>): Set<CommandData> {
|
groupingKey: Issued<T>?): Set<C> {
|
||||||
requireThat {
|
requireThat {
|
||||||
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
||||||
}
|
}
|
||||||
return emptySet()
|
return emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "No zero sized outputs"
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -6,15 +6,13 @@ import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER
|
|||||||
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
|
import com.r3corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
|
||||||
import com.r3corda.core.contracts.Amount
|
import com.r3corda.core.contracts.Amount
|
||||||
import com.r3corda.core.contracts.Issued
|
import com.r3corda.core.contracts.Issued
|
||||||
import com.r3corda.core.contracts.SignedTransaction
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
import com.r3corda.core.contracts.TransactionType
|
import com.r3corda.core.contracts.TransactionType
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.node.ServiceHub
|
import com.r3corda.core.node.ServiceHub
|
||||||
import com.r3corda.core.node.services.Wallet
|
import com.r3corda.core.node.services.Wallet
|
||||||
import com.r3corda.core.serialization.OpaqueBytes
|
import com.r3corda.core.serialization.OpaqueBytes
|
||||||
import com.r3corda.core.testing.DUMMY_NOTARY
|
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||||
import com.r3corda.core.testing.DUMMY_NOTARY_KEY
|
|
||||||
import java.security.KeyPair
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -61,22 +59,29 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount<Currency>,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray {
|
private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray {
|
||||||
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
|
val numSlots = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
|
||||||
val amounts = LongArray(numStates)
|
val baseSize = howMuch.quantity / numSlots
|
||||||
val baseSize = howMuch.quantity / numStates
|
|
||||||
check(baseSize > 0) { baseSize }
|
check(baseSize > 0) { baseSize }
|
||||||
var filledSoFar = 0L
|
|
||||||
for (i in 0..numStates - 1) {
|
val amounts = LongArray(numSlots) { baseSize }
|
||||||
if (i < numStates - 1) {
|
var distanceFromGoal = 0L
|
||||||
// Adjust the amount a bit up or down, to give more realistic amounts (not all identical).
|
// If we want 10 slots then max adjust is 0.1, so even if all random numbers come out to the largest downward
|
||||||
amounts[i] = baseSize + (baseSize / 2 * (rng.nextDouble() - 0.5)).toLong()
|
// adjustment possible, the last slot ends at zero. With 20 slots, max adjust is 0.05 etc.
|
||||||
filledSoFar += amounts[i]
|
val maxAdjust = 1.0 / numSlots
|
||||||
|
for (i in amounts.indices) {
|
||||||
|
if (i != amounts.lastIndex) {
|
||||||
|
val adjustBy = rng.nextDouble() * maxAdjust - (maxAdjust / 2)
|
||||||
|
val adjustment = (1 + adjustBy)
|
||||||
|
val adjustTo = (amounts[i] * adjustment).toLong()
|
||||||
|
amounts[i] = adjustTo
|
||||||
|
distanceFromGoal += baseSize - adjustTo
|
||||||
} else {
|
} else {
|
||||||
// Handle inexact rounding.
|
amounts[i] += distanceFromGoal
|
||||||
amounts[i] = howMuch.quantity - filledSoFar
|
|
||||||
}
|
}
|
||||||
check(amounts[i] >= 0) { "${amounts[i]} : $filledSoFar : $howMuch" }
|
|
||||||
}
|
}
|
||||||
check(amounts.sum() == howMuch.quantity)
|
|
||||||
|
// The desired amount may not have divided equally to start with, so adjust the first value to make up.
|
||||||
|
amounts[0] += howMuch.quantity - amounts.sum()
|
||||||
|
|
||||||
return amounts
|
return amounts
|
||||||
}
|
}
|
@ -7,16 +7,17 @@ import com.r3corda.core.contracts.*
|
|||||||
import com.r3corda.core.crypto.DigitalSignature
|
import com.r3corda.core.crypto.DigitalSignature
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.signWithECDSA
|
import com.r3corda.core.crypto.signWithECDSA
|
||||||
import com.r3corda.core.crypto.toStringsShort
|
|
||||||
import com.r3corda.core.node.NodeInfo
|
import com.r3corda.core.node.NodeInfo
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
import com.r3corda.core.random63BitValue
|
import com.r3corda.core.random63BitValue
|
||||||
import com.r3corda.core.seconds
|
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.ProgressTracker
|
||||||
import com.r3corda.core.utilities.trace
|
import com.r3corda.core.utilities.trace
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SignatureException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -95,7 +96,6 @@ object TwoPartyTradeProtocol {
|
|||||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||||
val ourSignature = signWithOurKey(partialTX)
|
val ourSignature = signWithOurKey(partialTX)
|
||||||
val notarySignature = getNotarySignature(partialTX)
|
val notarySignature = getNotarySignature(partialTX)
|
||||||
|
|
||||||
return sendSignatures(partialTX, ourSignature, notarySignature)
|
return sendSignatures(partialTX, ourSignature, notarySignature)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,16 +118,11 @@ object TwoPartyTradeProtocol {
|
|||||||
|
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
|
|
||||||
maybeSTX.validate {
|
maybeSTX.unwrap {
|
||||||
progressTracker.nextStep()
|
progressTracker.nextStep()
|
||||||
|
|
||||||
// Check that the tx proposed by the buyer is valid.
|
// Check that the tx proposed by the buyer is valid.
|
||||||
val missingSigs: Set<PublicKey> = it.verifySignatures(throwIfSignaturesAreMissing = false)
|
val wtx: WireTransaction = it.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
|
||||||
val expected = setOf(myKeyPair.public, notaryNode.identity.owningKey)
|
|
||||||
if (missingSigs != expected)
|
|
||||||
throw SignatureException("The set of missing signatures is not as expected: ${missingSigs.toStringsShort()} vs ${expected.toStringsShort()}")
|
|
||||||
|
|
||||||
val wtx: WireTransaction = it.tx
|
|
||||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
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,
|
// Download and check all the things that this transaction depends on and verify it is contract-valid,
|
||||||
@ -212,7 +207,7 @@ object TwoPartyTradeProtocol {
|
|||||||
val maybeTradeRequest = receive<SellerTradeInfo>(sessionID)
|
val maybeTradeRequest = receive<SellerTradeInfo>(sessionID)
|
||||||
|
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
maybeTradeRequest.validate {
|
maybeTradeRequest.unwrap {
|
||||||
// What is the seller trying to sell us?
|
// What is the seller trying to sell us?
|
||||||
val asset = it.assetForSale.state.data
|
val asset = it.assetForSale.state.data
|
||||||
val assetTypeName = asset.javaClass.name
|
val assetTypeName = asset.javaClass.name
|
||||||
@ -240,7 +235,7 @@ object TwoPartyTradeProtocol {
|
|||||||
|
|
||||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||||
|
|
||||||
return sendAndReceive<SignaturesFromSeller>(otherSide, theirSessionID, sessionID, stx).validate { it }
|
return sendAndReceive<SignaturesFromSeller>(otherSide, theirSessionID, sessionID, stx).unwrap { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||||
|
@ -6,7 +6,8 @@ import kotlin.*;
|
|||||||
import org.junit.*;
|
import org.junit.*;
|
||||||
|
|
||||||
import static com.r3corda.core.contracts.ContractsDSL.*;
|
import static com.r3corda.core.contracts.ContractsDSL.*;
|
||||||
import static com.r3corda.core.testing.CoreTestUtils.*;
|
import static com.r3corda.core.utilities.TestConstants.*;
|
||||||
|
import static com.r3corda.testing.CoreTestUtils.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL
|
* This is an incomplete Java replica of CashTests.kt to show how to use the Java test DSL
|
||||||
|
@ -6,9 +6,15 @@ import com.r3corda.core.contracts.*
|
|||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.days
|
import com.r3corda.core.days
|
||||||
import com.r3corda.core.node.services.testing.MockServices
|
|
||||||
import com.r3corda.core.seconds
|
import com.r3corda.core.seconds
|
||||||
import com.r3corda.core.testing.*
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||||
|
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
|
||||||
|
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
|
||||||
|
import com.r3corda.core.utilities.TEST_TX_TIME
|
||||||
|
import com.r3corda.testing.node.MockServices
|
||||||
|
import com.r3corda.testing.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.junit.runners.Parameterized
|
import org.junit.runners.Parameterized
|
||||||
@ -17,6 +23,8 @@ import java.util.*
|
|||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
// TODO: The generate functions aren't tested by these tests: add them.
|
||||||
|
|
||||||
interface ICommercialPaperTestTemplate {
|
interface ICommercialPaperTestTemplate {
|
||||||
fun getPaper(): ICommercialPaperState
|
fun getPaper(): ICommercialPaperState
|
||||||
fun getIssueCommand(notary: Party): CommandData
|
fun getIssueCommand(notary: Party): CommandData
|
||||||
@ -32,8 +40,8 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
|||||||
TEST_TX_TIME + 7.days
|
TEST_TX_TIME + 7.days
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue(notary)
|
override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue()
|
||||||
override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem(notary)
|
override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem()
|
||||||
override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move()
|
override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +53,8 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
|
|||||||
maturityDate = TEST_TX_TIME + 7.days
|
maturityDate = TEST_TX_TIME + 7.days
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue(notary)
|
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
|
||||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem(notary)
|
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
|
||||||
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +66,8 @@ class KotlinCommercialPaperLegacyTest() : ICommercialPaperTestTemplate {
|
|||||||
maturityDate = TEST_TX_TIME + 7.days
|
maturityDate = TEST_TX_TIME + 7.days
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue(notary)
|
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue()
|
||||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem(notary)
|
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem()
|
||||||
override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move()
|
override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
package com.r3corda.contracts
|
package com.r3corda.contracts
|
||||||
|
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.node.services.testing.MockServices
|
|
||||||
import com.r3corda.core.seconds
|
import com.r3corda.core.seconds
|
||||||
import com.r3corda.core.testing.*
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||||
|
import com.r3corda.core.utilities.DUMMY_NOTARY_KEY
|
||||||
|
import com.r3corda.core.utilities.TEST_TX_TIME
|
||||||
|
import com.r3corda.testing.LedgerDSL
|
||||||
|
import com.r3corda.testing.TestLedgerDSLInterpreter
|
||||||
|
import com.r3corda.testing.TestTransactionDSLInterpreter
|
||||||
|
import com.r3corda.testing.node.MockServices
|
||||||
|
import com.r3corda.testing.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@ -394,9 +401,10 @@ class IRSTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ensure failure occurs when there are inbound states for an agreement command`() {
|
fun `ensure failure occurs when there are inbound states for an agreement command`() {
|
||||||
|
val irs = singleIRS()
|
||||||
transaction {
|
transaction {
|
||||||
input() { singleIRS() }
|
input() { irs }
|
||||||
output("irs post agreement") { singleIRS() }
|
output("irs post agreement") { irs }
|
||||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
this `fails with` "There are no in states for an agreement"
|
this `fails with` "There are no in states for an agreement"
|
||||||
@ -665,10 +673,11 @@ class IRSTests {
|
|||||||
transaction("Agreement") {
|
transaction("Agreement") {
|
||||||
output("irs post agreement2") {
|
output("irs post agreement2") {
|
||||||
irs.copy(
|
irs.copy(
|
||||||
irs.fixedLeg,
|
linearId = UniqueIdentifier("t2"),
|
||||||
irs.floatingLeg,
|
fixedLeg = irs.fixedLeg,
|
||||||
irs.calculation,
|
floatingLeg = irs.floatingLeg,
|
||||||
irs.common.copy(tradeID = "t2")
|
calculation = irs.calculation,
|
||||||
|
common = irs.common.copy(tradeID = "t2")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||||
|
@ -4,7 +4,11 @@ import com.r3corda.core.contracts.*
|
|||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.serialization.OpaqueBytes
|
import com.r3corda.core.serialization.OpaqueBytes
|
||||||
import com.r3corda.core.testing.*
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
|
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||||
|
import com.r3corda.core.utilities.DUMMY_PUBKEY_1
|
||||||
|
import com.r3corda.core.utilities.DUMMY_PUBKEY_2
|
||||||
|
import com.r3corda.testing.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -17,7 +21,9 @@ class CashTests {
|
|||||||
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
||||||
owner = DUMMY_PUBKEY_1
|
owner = DUMMY_PUBKEY_1
|
||||||
)
|
)
|
||||||
val outState = inState.copy(owner = DUMMY_PUBKEY_2)
|
// Input state held by the issuer
|
||||||
|
val issuerInState = inState.copy(owner = defaultIssuer.party.owningKey)
|
||||||
|
val outState = issuerInState.copy(owner = DUMMY_PUBKEY_2)
|
||||||
|
|
||||||
fun Cash.State.editDepositRef(ref: Byte) = copy(
|
fun Cash.State.editDepositRef(ref: Byte) = copy(
|
||||||
amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref))))
|
amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref))))
|
||||||
@ -59,7 +65,7 @@ class CashTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun issueMoney() {
|
fun `issue by move`() {
|
||||||
// Check we can't "move" money into existence.
|
// Check we can't "move" money into existence.
|
||||||
transaction {
|
transaction {
|
||||||
input { DummyState() }
|
input { DummyState() }
|
||||||
@ -68,7 +74,10 @@ class CashTests {
|
|||||||
|
|
||||||
this `fails with` "there is at least one asset input"
|
this `fails with` "there is at least one asset input"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun issue() {
|
||||||
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
|
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
|
||||||
// institution is allowed to issue as much cash as they want.
|
// institution is allowed to issue as much cash as they want.
|
||||||
transaction {
|
transaction {
|
||||||
@ -90,28 +99,41 @@ class CashTests {
|
|||||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun generateIssueRaw() {
|
||||||
// Test generation works.
|
// Test generation works.
|
||||||
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
|
||||||
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||||
assertTrue(ptx.inputStates().isEmpty())
|
signWith(MINI_CORP_KEY)
|
||||||
val s = ptx.outputStates()[0].data as Cash.State
|
}.toSignedTransaction().tx
|
||||||
|
assertTrue(tx.inputs.isEmpty())
|
||||||
|
val s = tx.outputs[0].data as Cash.State
|
||||||
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
|
assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount)
|
||||||
assertEquals(MINI_CORP, s.deposit.party)
|
assertEquals(MINI_CORP, s.deposit.party)
|
||||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
||||||
assertTrue(ptx.commands()[0].value is Cash.Commands.Issue)
|
assertTrue(tx.commands[0].value is Cash.Commands.Issue)
|
||||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
|
||||||
|
}
|
||||||
|
|
||||||
// Test issuance from the issuance definition
|
@Test
|
||||||
|
fun generateIssueFromAmount() {
|
||||||
|
// Test issuance from an issued amount
|
||||||
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
|
val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34)
|
||||||
val templatePtx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
|
||||||
Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
Cash().generateIssue(this, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||||
assertTrue(templatePtx.inputStates().isEmpty())
|
signWith(MINI_CORP_KEY)
|
||||||
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0])
|
}.toSignedTransaction().tx
|
||||||
|
assertTrue(tx.inputs.isEmpty())
|
||||||
|
assertEquals(tx.outputs[0], tx.outputs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `extended issue examples`() {
|
||||||
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { issuerInState }
|
||||||
output { inState.copy(amount = inState.amount * 2) }
|
output { inState.copy(amount = inState.amount * 2) }
|
||||||
|
|
||||||
// Move fails: not allowed to summon money.
|
// Move fails: not allowed to summon money.
|
||||||
@ -154,11 +176,11 @@ class CashTests {
|
|||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||||
this `fails with` "All commands must be matched at end of execution."
|
this `fails with` "The following commands were not matched at the end of execution"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(inState.amount / 2) }
|
||||||
this `fails with` "All commands must be matched at end of execution."
|
this `fails with` "The following commands were not matched at the end of execution"
|
||||||
}
|
}
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
@ -279,12 +301,12 @@ class CashTests {
|
|||||||
fun exitLedger() {
|
fun exitLedger() {
|
||||||
// Single input/output straightforward case.
|
// Single input/output straightforward case.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { issuerInState }
|
||||||
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(100.DOLLARS `issued by` defaultIssuer) }
|
||||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||||
this `fails with` "the amounts balance"
|
this `fails with` "the amounts balance"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,20 +315,24 @@ class CashTests {
|
|||||||
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
|
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
|
||||||
|
|
||||||
tweak {
|
tweak {
|
||||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `exit ledger with multiple issuers`() {
|
||||||
// Multi-issuer case.
|
// Multi-issuer case.
|
||||||
transaction {
|
transaction {
|
||||||
input { inState }
|
input { issuerInState }
|
||||||
input { inState `issued by` MINI_CORP }
|
input { issuerInState.copy(owner = MINI_CORP_PUBKEY) `issued by` MINI_CORP }
|
||||||
|
|
||||||
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
|
output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
|
||||||
output { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
output { issuerInState.copy(owner = MINI_CORP_PUBKEY, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||||
|
|
||||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||||
|
|
||||||
this `fails with` "at issuer MegaCorp the amounts balance"
|
this `fails with` "at issuer MegaCorp the amounts balance"
|
||||||
|
|
||||||
@ -318,6 +344,18 @@ class CashTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `exit cash not held by its issuer`() {
|
||||||
|
// Single input/output straightforward case.
|
||||||
|
transaction {
|
||||||
|
input { inState }
|
||||||
|
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||||
|
command(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS `issued by` defaultIssuer) }
|
||||||
|
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||||
|
this `fails with` "at issuer MegaCorp the amounts balance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun multiIssuer() {
|
fun multiIssuer() {
|
||||||
transaction {
|
transaction {
|
||||||
@ -385,7 +423,7 @@ class CashTests {
|
|||||||
*/
|
*/
|
||||||
fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction {
|
fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction {
|
||||||
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val tx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||||
Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), OUR_PUBKEY_1, WALLET)
|
Cash().generateExit(tx, Amount(amount.quantity, Issued(corp.ref(depositRef), amount.token)), WALLET)
|
||||||
return tx.toWireTransaction()
|
return tx.toWireTransaction()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,9 +442,10 @@ class CashTests {
|
|||||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||||
assertEquals(0, wtx.outputs.size)
|
assertEquals(0, wtx.outputs.size)
|
||||||
|
|
||||||
val expected = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
|
val expectedMove = Cash.Commands.Move()
|
||||||
val actual = wtx.commands.single().value
|
val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
|
||||||
assertEquals(expected, actual)
|
|
||||||
|
assertEquals(listOf(expectedMove, expectedExit), wtx.commands.map { it.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,8 +5,8 @@ import com.r3corda.core.contracts.*
|
|||||||
import com.r3corda.core.crypto.NullPublicKey
|
import com.r3corda.core.crypto.NullPublicKey
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.serialization.OpaqueBytes
|
import com.r3corda.core.serialization.OpaqueBytes
|
||||||
import com.r3corda.core.testing.*
|
import com.r3corda.core.utilities.*
|
||||||
import com.r3corda.core.utilities.nonEmptySetOf
|
import com.r3corda.testing.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@ -119,19 +119,21 @@ class ObligationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test generation works.
|
// Test generation works.
|
||||||
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
val tx = TransactionType.General.Builder(notary = null).apply {
|
||||||
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
||||||
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||||
assertTrue(ptx.inputStates().isEmpty())
|
signWith(MINI_CORP_KEY)
|
||||||
|
}.toSignedTransaction().tx
|
||||||
|
assertTrue(tx.inputs.isEmpty())
|
||||||
val expected = Obligation.State(
|
val expected = Obligation.State(
|
||||||
obligor = MINI_CORP,
|
obligor = MINI_CORP,
|
||||||
quantity = 100.DOLLARS.quantity,
|
quantity = 100.DOLLARS.quantity,
|
||||||
beneficiary = DUMMY_PUBKEY_1,
|
beneficiary = DUMMY_PUBKEY_1,
|
||||||
template = megaCorpDollarSettlement
|
template = megaCorpDollarSettlement
|
||||||
)
|
)
|
||||||
assertEquals(ptx.outputStates()[0].data, expected)
|
assertEquals(tx.outputs[0].data, expected)
|
||||||
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue)
|
assertTrue(tx.commands[0].value is Obligation.Commands.Issue)
|
||||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0])
|
||||||
|
|
||||||
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
// We can consume $1000 in a transaction and output $2000 as long as it's signed by an issuer.
|
||||||
transaction {
|
transaction {
|
||||||
@ -178,11 +180,11 @@ class ObligationTests {
|
|||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Move() }
|
||||||
this `fails with` "All commands must be matched at end of execution."
|
this `fails with` "The following commands were not matched at the end of execution"
|
||||||
}
|
}
|
||||||
tweak {
|
tweak {
|
||||||
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) }
|
command(MEGA_CORP_PUBKEY) { Obligation.Commands.Exit(inState.amount / 2) }
|
||||||
this `fails with` "All commands must be matched at end of execution."
|
this `fails with` "The following commands were not matched at the end of execution"
|
||||||
}
|
}
|
||||||
this.verifies()
|
this.verifies()
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ dependencies {
|
|||||||
|
|
||||||
// Bring in the MockNode infrastructure for writing protocol unit tests.
|
// Bring in the MockNode infrastructure for writing protocol unit tests.
|
||||||
testCompile project(":node")
|
testCompile project(":node")
|
||||||
|
testCompile project(":test-utils")
|
||||||
|
|
||||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
@ -55,6 +56,9 @@ dependencies {
|
|||||||
// AssertJ: for fluent assertions for testing
|
// AssertJ: for fluent assertions for testing
|
||||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||||
|
|
||||||
|
compile 'com.pholser:junit-quickcheck-core:0.6'
|
||||||
|
compile 'com.pholser:junit-quickcheck-generators:0.6'
|
||||||
|
|
||||||
// Guava: Google utilities library.
|
// Guava: Google utilities library.
|
||||||
compile "com.google.guava:guava:19.0"
|
compile "com.google.guava:guava:19.0"
|
||||||
|
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
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)
|
||||||
|
}
|
@ -2,6 +2,7 @@ package com.r3corda.core.contracts
|
|||||||
|
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||||
@ -53,11 +54,13 @@ class DummyContract : Contract {
|
|||||||
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), 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): TransactionBuilder {
|
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
|
||||||
val priorState = prior.state.data
|
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)
|
val (cmd, state) = priorState.withNewOwner(newOwner)
|
||||||
return TransactionType.General.Builder(notary = prior.state.notary).withItems(
|
return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
|
||||||
/* INPUT */ prior,
|
/* INPUTS */ *priors.toTypedArray(),
|
||||||
/* COMMAND */ Command(cmd, priorState.owner),
|
/* COMMAND */ Command(cmd, priorState.owner),
|
||||||
/* OUTPUT */ state
|
/* OUTPUT */ state
|
||||||
)
|
)
|
||||||
|
@ -3,7 +3,7 @@ package com.r3corda.core.contracts
|
|||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dummy state for use in testing. Not part of any real contract.
|
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
|
||||||
*/
|
*/
|
||||||
data class DummyState(val magicNumber: Int = 0) : ContractState {
|
data class DummyState(val magicNumber: Int = 0) : ContractState {
|
||||||
override val contract = DUMMY_PROGRAM_ID
|
override val contract = DUMMY_PROGRAM_ID
|
||||||
|
@ -423,15 +423,43 @@ enum class NetType {
|
|||||||
PAYMENT
|
PAYMENT
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Commodity(val symbol: String,
|
/**
|
||||||
|
* Class representing a commodity, as an equivalent to the [Currency] class. This exists purely to enable the
|
||||||
|
* [CommodityContract] contract, and is likely to change in future.
|
||||||
|
*
|
||||||
|
* @param commodityCode a unique code for the commodity. No specific registry for these is currently defined, although
|
||||||
|
* this is likely to change in future.
|
||||||
|
* @param displayName human readable name for the commodity.
|
||||||
|
* @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of
|
||||||
|
* this commodity.
|
||||||
|
*/
|
||||||
|
data class Commodity(val commodityCode: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val commodityCode: String = symbol,
|
|
||||||
val defaultFractionDigits: Int = 0) {
|
val defaultFractionDigits: Int = 0) {
|
||||||
companion object {
|
companion object {
|
||||||
private val registry = mapOf(
|
private val registry = mapOf(
|
||||||
|
// Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp
|
||||||
Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice"))
|
Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice"))
|
||||||
)
|
)
|
||||||
fun getInstance(symbol: String): Commodity?
|
fun getInstance(commodityCode: String): Commodity?
|
||||||
= registry[symbol]
|
= registry[commodityCode]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides a truly unique identifier of a trade, state, or other business object.
|
||||||
|
* @param externalId If there is an existing weak identifer e.g. trade reference id.
|
||||||
|
* This should be set here the first time a UniqueIdentifier identifier is created as part of an issue,
|
||||||
|
* or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong id.
|
||||||
|
* @param id Should never be set by user code and left as default initialised.
|
||||||
|
* So that the first time a state is issued this should be given a new UUID.
|
||||||
|
* Subsequent copies and evolutions of a state should just copy the externalId and Id fields unmodified.
|
||||||
|
*/
|
||||||
|
data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) {
|
||||||
|
override fun toString(): String {
|
||||||
|
if (externalId != null) {
|
||||||
|
return "${externalId}_${id.toString()}"
|
||||||
|
}
|
||||||
|
return id.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.r3corda.core.contracts
|
package com.r3corda.core.contracts
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.clauses.Clause
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.crypto.toStringShort
|
import com.r3corda.core.crypto.toStringShort
|
||||||
@ -7,6 +8,7 @@ import com.r3corda.core.protocols.ProtocolLogicRef
|
|||||||
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
||||||
import com.r3corda.core.serialization.OpaqueBytes
|
import com.r3corda.core.serialization.OpaqueBytes
|
||||||
import com.r3corda.core.serialization.serialize
|
import com.r3corda.core.serialization.serialize
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
@ -215,16 +217,41 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan
|
|||||||
data class ScheduledActivity(val logicRef: ProtocolLogicRef, override val scheduledAt: Instant) : Scheduled
|
data class ScheduledActivity(val logicRef: ProtocolLogicRef, override val scheduledAt: Instant) : Scheduled
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A state that evolves by superseding itself, all of which share the common "thread".
|
* A state that evolves by superseding itself, all of which share the common "linearId".
|
||||||
*
|
*
|
||||||
* This simplifies the job of tracking the current version of certain types of state in e.g. a wallet.
|
* This simplifies the job of tracking the current version of certain types of state in e.g. a wallet.
|
||||||
*/
|
*/
|
||||||
interface LinearState : ContractState {
|
interface LinearState: ContractState {
|
||||||
/** Unique thread id within the wallets of all parties */
|
/**
|
||||||
val thread: SecureHash
|
* Unique id shared by all LinearState states throughout history within the wallets of all parties.
|
||||||
|
* Verify methods should check that one input and one output share the id in a transaction,
|
||||||
|
* except at issuance/termination.
|
||||||
|
*/
|
||||||
|
val linearId: UniqueIdentifier
|
||||||
|
|
||||||
/** true if this should be tracked by our wallet(s) */
|
/**
|
||||||
|
* True if this should be tracked by our wallet(s).
|
||||||
|
* */
|
||||||
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
|
fun isRelevant(ourKeys: Set<PublicKey>): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard clause to verify the LinearState safety properties.
|
||||||
|
*/
|
||||||
|
class ClauseVerifier<S : LinearState>(val stateClass: Class<S>) : Clause<ContractState, CommandData, Unit>() {
|
||||||
|
override fun verify(tx: TransactionForContract,
|
||||||
|
inputs: List<ContractState>,
|
||||||
|
outputs: List<ContractState>,
|
||||||
|
commands: List<AuthenticatedObject<CommandData>>,
|
||||||
|
groupingKey: Unit?): Set<CommandData> {
|
||||||
|
val filteredInputs = inputs.filterIsInstance(stateClass)
|
||||||
|
val inputIds = filteredInputs.map { it.linearId }.distinct()
|
||||||
|
require(inputIds.count() == filteredInputs.count()) { "LinearStates cannot be merged" }
|
||||||
|
val filteredOutputs = outputs.filterIsInstance(stateClass)
|
||||||
|
val outputIds = filteredOutputs.map { it.linearId }.distinct()
|
||||||
|
require(outputIds.count() == filteredOutputs.count()) { "LinearStates cannot be split" }
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SchedulableState : ContractState {
|
interface SchedulableState : ContractState {
|
||||||
@ -348,6 +375,12 @@ interface MoveCommand : CommandData {
|
|||||||
val contractHash: SecureHash?
|
val contractHash: SecureHash?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A common netting command for contracts whose states can be netted. */
|
||||||
|
interface NetCommand : CommandData {
|
||||||
|
/** The type of netting to apply, see [NetType] for options. */
|
||||||
|
val type: NetType
|
||||||
|
}
|
||||||
|
|
||||||
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
|
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
|
||||||
data class AuthenticatedObject<out T : Any>(
|
data class AuthenticatedObject<out T : Any>(
|
||||||
val signers: List<PublicKey>,
|
val signers: List<PublicKey>,
|
||||||
|
@ -2,6 +2,8 @@ package com.r3corda.core.contracts
|
|||||||
|
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.node.services.ReadOnlyTransactionStorage
|
import com.r3corda.core.node.services.ReadOnlyTransactionStorage
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
package com.r3corda.core.contracts
|
|
||||||
|
|
||||||
import com.r3corda.core.node.ServiceHub
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
// TODO: Move these into the actual classes (i.e. where people would expect to find them) and split Transactions.kt into multiple files
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
|
|
||||||
* have been fully resolved using the resolution protocol by this point.
|
|
||||||
*
|
|
||||||
* @throws FileNotFoundException if a required attachment was not found in storage.
|
|
||||||
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
|
||||||
*/
|
|
||||||
fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
|
|
||||||
// Look up random keys to authenticated identities. This is just a stub placeholder and will all change in future.
|
|
||||||
val authenticatedArgs = commands.map {
|
|
||||||
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
|
|
||||||
AuthenticatedObject(it.signers, parties, it.value)
|
|
||||||
}
|
|
||||||
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
|
|
||||||
val attachments = attachments.map {
|
|
||||||
services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString())
|
|
||||||
}
|
|
||||||
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) }
|
|
||||||
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, signers, timestamp, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls [verify] to check all required signatures are present, and then calls [WireTransaction.toLedgerTransaction]
|
|
||||||
* with the passed in [ServiceHub] to resolve the dependencies, returning an unverified LedgerTransaction.
|
|
||||||
*
|
|
||||||
* @throws FileNotFoundException if a required attachment was not found in storage.
|
|
||||||
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
|
||||||
*/
|
|
||||||
fun SignedTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
|
|
||||||
verifySignatures()
|
|
||||||
return tx.toLedgerTransaction(services)
|
|
||||||
}
|
|
@ -1,7 +1,8 @@
|
|||||||
package com.r3corda.core.contracts
|
package com.r3corda.core.contracts
|
||||||
|
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.noneOrSingle
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/** Defines transaction build & validation logic for a specific transaction type */
|
/** Defines transaction build & validation logic for a specific transaction type */
|
||||||
@ -29,7 +30,7 @@ sealed class TransactionType {
|
|||||||
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
|
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
|
||||||
|
|
||||||
val requiredKeys = getRequiredSigners(tx) + notaryKey
|
val requiredKeys = getRequiredSigners(tx) + notaryKey
|
||||||
val missing = requiredKeys - tx.signers
|
val missing = requiredKeys - tx.mustSign
|
||||||
|
|
||||||
return missing
|
return missing
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package com.r3corda.core.contracts
|
|||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.crypto.toStringShort
|
import com.r3corda.core.crypto.toStringShort
|
||||||
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -1,218 +0,0 @@
|
|||||||
package com.r3corda.core.contracts
|
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo
|
|
||||||
import com.r3corda.core.crypto.DigitalSignature
|
|
||||||
import com.r3corda.core.crypto.Party
|
|
||||||
import com.r3corda.core.crypto.SecureHash
|
|
||||||
import com.r3corda.core.crypto.toStringsShort
|
|
||||||
import com.r3corda.core.indexOfOrThrow
|
|
||||||
import com.r3corda.core.serialization.SerializedBytes
|
|
||||||
import com.r3corda.core.serialization.THREAD_LOCAL_KRYO
|
|
||||||
import com.r3corda.core.serialization.deserialize
|
|
||||||
import com.r3corda.core.serialization.serialize
|
|
||||||
import com.r3corda.core.utilities.Emoji
|
|
||||||
import java.security.PublicKey
|
|
||||||
import java.security.SignatureException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Views of a transaction as it progresses through the pipeline, from bytes loaded from disk/network to the object
|
|
||||||
* tree passed into a contract.
|
|
||||||
*
|
|
||||||
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
|
|
||||||
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
|
|
||||||
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash
|
|
||||||
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may
|
|
||||||
* map to the same key (and they could be different in important ways, like validity!). The signatures on a
|
|
||||||
* SignedTransaction might be invalid or missing: the type does not imply validity.
|
|
||||||
*
|
|
||||||
* WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed
|
|
||||||
* in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved
|
|
||||||
* keypairs.
|
|
||||||
*
|
|
||||||
* LedgerTransaction is derived from WireTransaction. It is the result of doing the following operations:
|
|
||||||
*
|
|
||||||
* - Downloading and locally storing all the dependencies of the transaction.
|
|
||||||
* - Resolving the input states and loading them into memory.
|
|
||||||
* - Doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting the
|
|
||||||
* WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map.
|
|
||||||
* In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
|
|
||||||
* - Deserialising the output states.
|
|
||||||
*
|
|
||||||
* All the above refer to inputs using a (txhash, output index) pair.
|
|
||||||
*
|
|
||||||
* There is also TransactionForContract, which is a lightly red-acted form of LedgerTransaction that's fed into the
|
|
||||||
* contract's verify function. It may be removed in future.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Transaction ready for serialisation, without any signatures attached. */
|
|
||||||
data class WireTransaction(val inputs: List<StateRef>,
|
|
||||||
val attachments: List<SecureHash>,
|
|
||||||
val outputs: List<TransactionState<ContractState>>,
|
|
||||||
val commands: List<Command>,
|
|
||||||
val notary: Party?,
|
|
||||||
val signers: List<PublicKey>,
|
|
||||||
val type: TransactionType,
|
|
||||||
val timestamp: Timestamp?) : NamedByHash {
|
|
||||||
|
|
||||||
// Cache the serialised form of the transaction and its hash to give us fast access to it.
|
|
||||||
@Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null
|
|
||||||
val serialized: SerializedBytes<WireTransaction> get() = cachedBits ?: serialize().apply { cachedBits = this }
|
|
||||||
override val id: SecureHash get() = serialized.hash
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun deserialize(bits: SerializedBytes<WireTransaction>, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction {
|
|
||||||
val wtx = bits.bits.deserialize<WireTransaction>(kryo)
|
|
||||||
wtx.cachedBits = bits
|
|
||||||
return wtx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a [StateAndRef] for the given output index. */
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : ContractState> outRef(index: Int): StateAndRef<T> {
|
|
||||||
require(index >= 0 && index < outputs.size)
|
|
||||||
return StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */
|
|
||||||
fun <T : ContractState> outRef(state: ContractState): StateAndRef<T> = outRef(outputs.map { it.data }.indexOfOrThrow(state))
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
val buf = StringBuilder()
|
|
||||||
buf.appendln("Transaction $id:")
|
|
||||||
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
|
|
||||||
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
|
|
||||||
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
|
|
||||||
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
|
|
||||||
return buf.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Container for a [WireTransaction] and attached signatures. */
|
|
||||||
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
|
||||||
val sigs: List<DigitalSignature.WithKey>) : NamedByHash {
|
|
||||||
init {
|
|
||||||
check(sigs.isNotEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This needs to be reworked to ensure that the inner WireTransaction is only ever deserialised sandboxed.
|
|
||||||
|
|
||||||
/** Lazily calculated access to the deserialised/hashed transaction data. */
|
|
||||||
val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) }
|
|
||||||
|
|
||||||
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
|
|
||||||
override val id: SecureHash get() = txBits.hash
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains
|
|
||||||
* the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or
|
|
||||||
* returns the list of keys that have missing signatures, depending on the parameter.
|
|
||||||
*
|
|
||||||
* @throws SignatureException if a signature is invalid, does not match or if any signature is missing.
|
|
||||||
*/
|
|
||||||
fun verifySignatures(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
|
|
||||||
// Embedded WireTransaction is not deserialised until after we check the signatures.
|
|
||||||
for (sig in sigs)
|
|
||||||
sig.verifyWithECDSA(txBits.bits)
|
|
||||||
|
|
||||||
// Now examine the contents and ensure the sigs we have line up with the advertised list of signers.
|
|
||||||
val missing = getMissingSignatures()
|
|
||||||
if (missing.isNotEmpty() && throwIfSignaturesAreMissing) {
|
|
||||||
val missingElements = getMissingKeyDescriptions(missing)
|
|
||||||
throw SignatureException("Missing signatures for ${missingElements} on transaction ${id.prefixChars()} for ${missing.toStringsShort()}")
|
|
||||||
}
|
|
||||||
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a human readable description of where signatures are required from, and are missing, to assist in debugging
|
|
||||||
* the underlying cause.
|
|
||||||
*/
|
|
||||||
private fun getMissingKeyDescriptions(missing: Set<PublicKey>): ArrayList<String> {
|
|
||||||
// TODO: We need a much better way of structuring this data
|
|
||||||
val missingElements = ArrayList<String>()
|
|
||||||
this.tx.commands.forEach { command ->
|
|
||||||
if (command.signers.any { signer -> missing.contains(signer) })
|
|
||||||
missingElements.add(command.toString())
|
|
||||||
}
|
|
||||||
this.tx.notary?.owningKey.apply {
|
|
||||||
if (missing.contains(this))
|
|
||||||
missingElements.add("notary")
|
|
||||||
}
|
|
||||||
return missingElements
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the same transaction but with an additional (unchecked) signature */
|
|
||||||
fun withAdditionalSignature(sig: DigitalSignature.WithKey): SignedTransaction {
|
|
||||||
// TODO: need to make sure the Notary signs last
|
|
||||||
return copy(sigs = sigs + sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withAdditionalSignatures(sigList: Iterable<DigitalSignature.WithKey>): SignedTransaction {
|
|
||||||
return copy(sigs = sigs + sigList)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
|
|
||||||
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
|
|
||||||
|
|
||||||
operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the set of missing signatures - a signature must be present for each signer public key.
|
|
||||||
*/
|
|
||||||
private fun getMissingSignatures(): Set<PublicKey> {
|
|
||||||
val notaryKey = tx.notary?.owningKey
|
|
||||||
val requiredKeys = tx.signers.toSet()
|
|
||||||
val sigKeys = sigs.map { it.by }.toSet()
|
|
||||||
|
|
||||||
if (sigKeys.containsAll(requiredKeys)) return emptySet()
|
|
||||||
return requiredKeys - sigKeys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
|
|
||||||
* It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up
|
|
||||||
* with the commands from the wire, and verified/looked up.
|
|
||||||
*/
|
|
||||||
data class LedgerTransaction(
|
|
||||||
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
|
||||||
val inputs: List<StateAndRef<*>>,
|
|
||||||
/** The states that will be generated by the execution of this transaction. */
|
|
||||||
val outputs: List<TransactionState<*>>,
|
|
||||||
/** Arbitrary data passed to the program of each input state. */
|
|
||||||
val commands: List<AuthenticatedObject<CommandData>>,
|
|
||||||
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
|
|
||||||
val attachments: List<Attachment>,
|
|
||||||
/** The hash of the original serialised WireTransaction. */
|
|
||||||
override val id: SecureHash,
|
|
||||||
/** The notary for this party, may be null for transactions with no notary. */
|
|
||||||
val notary: Party?,
|
|
||||||
/** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */
|
|
||||||
val signers: List<PublicKey>,
|
|
||||||
val timestamp: Timestamp?,
|
|
||||||
val type: TransactionType
|
|
||||||
) : NamedByHash {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
|
|
||||||
|
|
||||||
// TODO: Remove this concept.
|
|
||||||
// There isn't really a good justification for hiding this data from the contract, it's just a backwards compat hack.
|
|
||||||
/** Strips the transaction down to a form that is usable by the contract verify functions */
|
|
||||||
fun toTransactionForContract(): TransactionForContract {
|
|
||||||
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
|
|
||||||
inputs.map { it.state.notary }.singleOrNull(), timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies this transaction and throws an exception if not valid, depending on the type. For general transactions:
|
|
||||||
*
|
|
||||||
* - The contracts are run with the transaction as the input.
|
|
||||||
* - The list of keys mentioned in commands is compared against the signers list.
|
|
||||||
*
|
|
||||||
* @throws TransactionVerificationException if anything goes wrong.
|
|
||||||
*/
|
|
||||||
fun verify() = type.verify(this)
|
|
||||||
}
|
|
@ -0,0 +1,38 @@
|
|||||||
|
package com.r3corda.core.contracts.clauses
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.AuthenticatedObject
|
||||||
|
import com.r3corda.core.contracts.CommandData
|
||||||
|
import com.r3corda.core.contracts.ContractState
|
||||||
|
import com.r3corda.core.contracts.TransactionForContract
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose a number of clauses, such that all of the clauses must run for verification to pass.
|
||||||
|
*/
|
||||||
|
class AllComposition<S : ContractState, C : CommandData, K : Any>(firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||||
|
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
clauses.add(firstClause)
|
||||||
|
clauses.addAll(remainingClauses)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> {
|
||||||
|
clauses.forEach { clause ->
|
||||||
|
check(clause.matches(commands)) { "Failed to match clause ${clause}" }
|
||||||
|
}
|
||||||
|
return clauses
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract,
|
||||||
|
inputs: List<S>,
|
||||||
|
outputs: List<S>,
|
||||||
|
commands: List<AuthenticatedObject<C>>,
|
||||||
|
groupingKey: K?): Set<C> {
|
||||||
|
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||||
|
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString() = "All: $clauses.toList()"
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.r3corda.core.contracts.clauses
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.AuthenticatedObject
|
||||||
|
import com.r3corda.core.contracts.CommandData
|
||||||
|
import com.r3corda.core.contracts.ContractState
|
||||||
|
import com.r3corda.core.contracts.TransactionForContract
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose a number of clauses, such that any number of the clauses can run.
|
||||||
|
*/
|
||||||
|
class AnyComposition<in S : ContractState, C : CommandData, in K : Any>(vararg rawClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||||
|
override val clauses: List<Clause<S, C, K>> = rawClauses.asList()
|
||||||
|
|
||||||
|
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = clauses.filter { it.matches(commands) }
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C> {
|
||||||
|
return matchedClauses(commands).flatMapTo(HashSet<C>()) { clause ->
|
||||||
|
clause.verify(tx, inputs, outputs, commands, groupingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "Or: ${clauses.toList()}"
|
||||||
|
}
|
@ -2,43 +2,68 @@ package com.r3corda.core.contracts.clauses
|
|||||||
|
|
||||||
import com.r3corda.core.contracts.AuthenticatedObject
|
import com.r3corda.core.contracts.AuthenticatedObject
|
||||||
import com.r3corda.core.contracts.CommandData
|
import com.r3corda.core.contracts.CommandData
|
||||||
|
import com.r3corda.core.contracts.ContractState
|
||||||
import com.r3corda.core.contracts.TransactionForContract
|
import com.r3corda.core.contracts.TransactionForContract
|
||||||
|
import com.r3corda.core.utilities.loggerFor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A clause that can be matched as part of execution of a contract.
|
* A clause of a contract, containing a chunk of verification logic. That logic may be delegated to other clauses, or
|
||||||
|
* provided directly by this clause.
|
||||||
|
*
|
||||||
|
* @param S the type of contract state this clause operates on.
|
||||||
|
* @param C a common supertype of commands this clause operates on.
|
||||||
|
* @param K the type of the grouping key for states this clause operates on. Use [Unit] if not applicable.
|
||||||
|
*
|
||||||
|
* @see CompositeClause
|
||||||
*/
|
*/
|
||||||
// TODO: ifNotMatched/ifMatched should be dropped, and replaced by logic in the calling code that understands
|
abstract class Clause<in S : ContractState, C : CommandData, in K : Any> {
|
||||||
// "or", "and", "single" etc. composition of sets of clauses.
|
companion object {
|
||||||
interface Clause {
|
val log = loggerFor<Clause<*, *, *>>()
|
||||||
/** Classes for commands which must ALL be present in transaction for this clause to be triggered */
|
}
|
||||||
val requiredCommands: Set<Class<out CommandData>>
|
|
||||||
/** Behaviour if this clause is matched */
|
|
||||||
val ifNotMatched: MatchBehaviour
|
|
||||||
/** Behaviour if this clause is not matches */
|
|
||||||
val ifMatched: MatchBehaviour
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class MatchBehaviour {
|
/** Determine whether this clause runs or not */
|
||||||
CONTINUE,
|
open val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
||||||
END,
|
|
||||||
ERROR
|
/**
|
||||||
}
|
* Determine the subclauses which will be verified as a result of verifying this clause.
|
||||||
|
*/
|
||||||
|
open fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||||
|
= listOf(this)
|
||||||
|
|
||||||
interface SingleVerify {
|
|
||||||
/**
|
/**
|
||||||
* Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause
|
* Verify the transaction matches the conditions from this clause. For example, a "no zero amount output" clause
|
||||||
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
|
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
|
||||||
* if any matched.
|
* if any matched.
|
||||||
*
|
*
|
||||||
|
* @param tx the full transaction being verified. This is provided for cases where clauses need to access
|
||||||
|
* states or commands outside of their normal scope.
|
||||||
|
* @param inputs input states which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||||
|
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||||
|
* @param outputs output states which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||||
|
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||||
|
* @param commands commands which are relevant to this clause. By default this is the set passed into [verifyClause],
|
||||||
|
* but may be further reduced by clauses such as [GroupClauseVerifier].
|
||||||
|
* @param groupingKey a grouping key applied to states and commands, where applicable. Taken from
|
||||||
|
* [TransactionForContract.InOutGroup].
|
||||||
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
|
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
|
||||||
* later clause. This would normally be all commands matching "requiredCommands" for this clause, but some
|
* later clause. This would normally be all commands matching "requiredCommands" for this clause, but some
|
||||||
* verify() functions may do further filtering on possible matches, and return a subset. This may also include
|
* verify() functions may do further filtering on possible matches, and return a subset. This may also include
|
||||||
* commands that were not required (for example the Exit command for fungible assets is optional).
|
* commands that were not required (for example the Exit command for fungible assets is optional).
|
||||||
*/
|
*/
|
||||||
@Throws(IllegalStateException::class)
|
@Throws(IllegalStateException::class)
|
||||||
fun verify(tx: TransactionForContract,
|
abstract fun verify(tx: TransactionForContract,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
inputs: List<S>,
|
||||||
|
outputs: List<S>,
|
||||||
|
commands: List<AuthenticatedObject<C>>,
|
||||||
|
groupingKey: K?): Set<C>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SingleClause : Clause, SingleVerify
|
/**
|
||||||
|
* Determine if the given list of commands matches the required commands for a clause to trigger.
|
||||||
|
*/
|
||||||
|
fun <C : CommandData> Clause<*, C, *>.matches(commands: List<AuthenticatedObject<C>>): Boolean {
|
||||||
|
return if (requiredCommands.isEmpty())
|
||||||
|
true
|
||||||
|
else
|
||||||
|
commands.map { it.value.javaClass }.toSet().containsAll(requiredCommands)
|
||||||
|
}
|
@ -2,9 +2,7 @@
|
|||||||
package com.r3corda.core.contracts.clauses
|
package com.r3corda.core.contracts.clauses
|
||||||
|
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
// Wrapper object for exposing a JVM friend version of the clause verifier
|
|
||||||
/**
|
/**
|
||||||
* Verify a transaction against the given list of clauses.
|
* Verify a transaction against the given list of clauses.
|
||||||
*
|
*
|
||||||
@ -13,27 +11,15 @@ import java.util.*
|
|||||||
* @param commands commands extracted from the transaction, which are relevant to the
|
* @param commands commands extracted from the transaction, which are relevant to the
|
||||||
* clauses.
|
* clauses.
|
||||||
*/
|
*/
|
||||||
fun verifyClauses(tx: TransactionForContract,
|
fun <C: CommandData> verifyClause(tx: TransactionForContract,
|
||||||
clauses: List<SingleClause>,
|
clause: Clause<ContractState, C, Unit>,
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>) {
|
commands: List<AuthenticatedObject<C>>) {
|
||||||
val unmatchedCommands = ArrayList(commands.map { it.value })
|
if (Clause.log.isTraceEnabled) {
|
||||||
|
clause.getExecutionPath(commands).forEach {
|
||||||
verify@ for (clause in clauses) {
|
Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
|
||||||
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
|
|
||||||
unmatchedCommands.removeAll(clause.verify(tx, commands))
|
|
||||||
clause.ifMatched
|
|
||||||
} else {
|
|
||||||
clause.ifNotMatched
|
|
||||||
}
|
|
||||||
|
|
||||||
when (matchBehaviour) {
|
|
||||||
MatchBehaviour.ERROR -> throw IllegalStateException()
|
|
||||||
MatchBehaviour.CONTINUE -> {
|
|
||||||
}
|
|
||||||
MatchBehaviour.END -> break@verify
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)
|
||||||
|
|
||||||
require(unmatchedCommands.isEmpty()) { "All commands must be matched at end of execution." }
|
check(matchedCommands.containsAll(commands.map { it.value })) { "The following commands were not matched at the end of execution: " + (commands - matchedCommands) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
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>>
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.r3corda.core.contracts.clauses
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.AuthenticatedObject
|
||||||
|
import com.r3corda.core.contracts.CommandData
|
||||||
|
import com.r3corda.core.contracts.ContractState
|
||||||
|
import com.r3corda.core.contracts.TransactionForContract
|
||||||
|
import com.r3corda.core.utilities.loggerFor
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose a number of clauses, such that the first match is run, and it errors if none is run.
|
||||||
|
*/
|
||||||
|
class FirstComposition<S : ContractState, C : CommandData, K : Any>(val firstClause: Clause<S, C, K>, vararg remainingClauses: Clause<S, C, K>) : CompositeClause<S, C, K>() {
|
||||||
|
companion object {
|
||||||
|
val logger = loggerFor<FirstComposition<*, *, *>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val clauses = ArrayList<Clause<S, C, K>>()
|
||||||
|
override fun matchedClauses(commands: List<AuthenticatedObject<C>>): List<Clause<S, C, K>> = listOf(clauses.first { it.matches(commands) })
|
||||||
|
|
||||||
|
init {
|
||||||
|
clauses.add(firstClause)
|
||||||
|
clauses.addAll(remainingClauses)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun verify(tx: TransactionForContract, inputs: List<S>, outputs: List<S>, commands: List<AuthenticatedObject<C>>, groupingKey: K?): Set<C>
|
||||||
|
= matchedClauses(commands).single().verify(tx, inputs, outputs, commands, groupingKey)
|
||||||
|
|
||||||
|
override fun toString() = "First: ${clauses.toList()}"
|
||||||
|
}
|
@ -6,77 +6,24 @@ import com.r3corda.core.contracts.ContractState
|
|||||||
import com.r3corda.core.contracts.TransactionForContract
|
import com.r3corda.core.contracts.TransactionForContract
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
interface GroupVerify<in S, in T : Any> {
|
abstract class GroupClauseVerifier<S : ContractState, C : CommandData, K : Any>(val clause: Clause<S, C, K>) : Clause<ContractState, C, Unit>() {
|
||||||
/**
|
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, K>>
|
||||||
*
|
|
||||||
* @return the set of commands that are consumed IF this clause is matched, and cannot be used to match a
|
|
||||||
* later clause.
|
|
||||||
*/
|
|
||||||
fun verify(tx: TransactionForContract,
|
|
||||||
inputs: List<S>,
|
|
||||||
outputs: List<S>,
|
|
||||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
|
||||||
token: T): Set<CommandData>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GroupClause<in S : ContractState, in T : Any> : Clause, GroupVerify<S, T>
|
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||||
|
= clause.getExecutionPath(commands)
|
||||||
|
|
||||||
abstract class GroupClauseVerifier<S : ContractState, T : Any> : SingleClause {
|
override fun verify(tx: TransactionForContract,
|
||||||
abstract val clauses: List<GroupClause<S, T>>
|
inputs: List<ContractState>,
|
||||||
override val requiredCommands: Set<Class<out CommandData>>
|
outputs: List<ContractState>,
|
||||||
get() = emptySet()
|
commands: List<AuthenticatedObject<C>>,
|
||||||
|
groupingKey: Unit?): Set<C> {
|
||||||
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
|
||||||
val groups = groupStates(tx)
|
val groups = groupStates(tx)
|
||||||
val matchedCommands = HashSet<CommandData>()
|
val matchedCommands = HashSet<C>()
|
||||||
val unmatchedCommands = ArrayList(commands.map { it.value })
|
|
||||||
|
|
||||||
for ((inputs, outputs, token) in groups) {
|
for ((groupInputs, groupOutputs, groupToken) in groups) {
|
||||||
val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands)
|
matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken))
|
||||||
matchedCommands.addAll(temp)
|
|
||||||
unmatchedCommands.removeAll(temp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchedCommands
|
return matchedCommands
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify a subset of a transaction's inputs and outputs matches the conditions from this clause. For example, a
|
|
||||||
* "no zero amount output" clause would check each of the output states within the group, looking for a zero amount,
|
|
||||||
* and throw IllegalStateException if any matched.
|
|
||||||
*
|
|
||||||
* @param commands the full set of commands which apply to this contract.
|
|
||||||
* @param inputs input states within this group.
|
|
||||||
* @param outputs output states within this group.
|
|
||||||
* @param token the object used as a key when grouping states.
|
|
||||||
* @param unmatchedCommands commands which have not yet been matched within this group.
|
|
||||||
* @return matchedCommands commands which are matched during the verification process.
|
|
||||||
*/
|
|
||||||
@Throws(IllegalStateException::class)
|
|
||||||
private fun verifyGroup(commands: Collection<AuthenticatedObject<CommandData>>,
|
|
||||||
inputs: List<S>,
|
|
||||||
outputs: List<S>,
|
|
||||||
token: T,
|
|
||||||
tx: TransactionForContract,
|
|
||||||
unmatchedCommands: List<CommandData>): Set<CommandData> {
|
|
||||||
val matchedCommands = HashSet<CommandData>()
|
|
||||||
verify@ for (clause in clauses) {
|
|
||||||
val matchBehaviour = if (unmatchedCommands.map { command -> command.javaClass }.containsAll(clause.requiredCommands)) {
|
|
||||||
matchedCommands.addAll(clause.verify(tx, inputs, outputs, commands, token))
|
|
||||||
clause.ifMatched
|
|
||||||
} else {
|
|
||||||
clause.ifNotMatched
|
|
||||||
}
|
|
||||||
|
|
||||||
when (matchBehaviour) {
|
|
||||||
MatchBehaviour.ERROR -> throw IllegalStateException()
|
|
||||||
MatchBehaviour.CONTINUE -> {
|
|
||||||
}
|
|
||||||
MatchBehaviour.END -> break@verify
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchedCommands
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,28 +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.TransactionForContract
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A clause which intercepts calls to a wrapped clause, and passes them through verification
|
|
||||||
* only from a pre-clause. This is similar to an inceptor in aspect orientated programming.
|
|
||||||
*/
|
|
||||||
data class InterceptorClause(
|
|
||||||
val preclause: SingleVerify,
|
|
||||||
val clause: SingleClause
|
|
||||||
) : SingleClause {
|
|
||||||
override val ifNotMatched: MatchBehaviour
|
|
||||||
get() = clause.ifNotMatched
|
|
||||||
override val ifMatched: MatchBehaviour
|
|
||||||
get() = clause.ifMatched
|
|
||||||
override val requiredCommands: Set<Class<out CommandData>>
|
|
||||||
get() = clause.requiredCommands
|
|
||||||
|
|
||||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
|
||||||
val consumed = HashSet(preclause.verify(tx, commands))
|
|
||||||
consumed.addAll(clause.verify(tx, commands))
|
|
||||||
return consumed
|
|
||||||
}
|
|
||||||
}
|
|
@ -36,7 +36,9 @@ object X509Utilities {
|
|||||||
|
|
||||||
val SIGNATURE_ALGORITHM = "SHA256withECDSA"
|
val SIGNATURE_ALGORITHM = "SHA256withECDSA"
|
||||||
val KEY_GENERATION_ALGORITHM = "ECDSA"
|
val KEY_GENERATION_ALGORITHM = "ECDSA"
|
||||||
val ECDSA_CURVE = "secp256k1" // TLS implementations only support standard SEC2 curves, although internally Corda uses newer EDDSA keys
|
// TLS implementations only support standard SEC2 curves, although internally Corda uses newer EDDSA keys.
|
||||||
|
// Also browsers like Chrome don't seem to support the secp256k1, only the secp256r1 curve.
|
||||||
|
val ECDSA_CURVE = "secp256r1"
|
||||||
|
|
||||||
val KEYSTORE_TYPE = "JKS"
|
val KEYSTORE_TYPE = "JKS"
|
||||||
val CA_CERT_ALIAS = "CA Cert"
|
val CA_CERT_ALIAS = "CA Cert"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package com.r3corda.core.node
|
package com.r3corda.core.node
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.r3corda.core.contracts.SignedTransaction
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
import com.r3corda.core.contracts.StateRef
|
import com.r3corda.core.contracts.StateRef
|
||||||
import com.r3corda.core.contracts.TransactionResolutionException
|
import com.r3corda.core.contracts.TransactionResolutionException
|
||||||
import com.r3corda.core.contracts.TransactionState
|
import com.r3corda.core.contracts.TransactionState
|
||||||
|
@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture
|
|||||||
import com.r3corda.core.contracts.Contract
|
import com.r3corda.core.contracts.Contract
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.messaging.MessagingService
|
import com.r3corda.core.messaging.MessagingService
|
||||||
|
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||||
import com.r3corda.core.node.NodeInfo
|
import com.r3corda.core.node.NodeInfo
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -20,8 +21,8 @@ interface NetworkMapCache {
|
|||||||
val logger = LoggerFactory.getLogger(NetworkMapCache::class.java)
|
val logger = LoggerFactory.getLogger(NetworkMapCache::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MapChangeType { Added, Removed }
|
enum class MapChangeType { Added, Removed, Modified }
|
||||||
data class MapChange(val node: NodeInfo, val type: MapChangeType )
|
data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType )
|
||||||
|
|
||||||
/** A list of nodes that advertise a network map service */
|
/** A list of nodes that advertise a network map service */
|
||||||
val networkMapNodes: List<NodeInfo>
|
val networkMapNodes: List<NodeInfo>
|
||||||
@ -73,12 +74,12 @@ interface NetworkMapCache {
|
|||||||
* updates.
|
* updates.
|
||||||
*
|
*
|
||||||
* @param net the network messaging service.
|
* @param net the network messaging service.
|
||||||
* @param service the network map service to fetch current state from.
|
* @param networkMapAddress the network map service to fetch current state from.
|
||||||
* @param subscribe if the cache should subscribe to updates.
|
* @param subscribe if the cache should subscribe to updates.
|
||||||
* @param ifChangedSinceVer an optional version number to limit updating the map based on. If the latest map
|
* @param ifChangedSinceVer an optional version number to limit updating the map based on. If the latest map
|
||||||
* version is less than or equal to the given version, no update is fetched.
|
* version is less than or equal to the given version, no update is fetched.
|
||||||
*/
|
*/
|
||||||
fun addMapService(net: MessagingService, service: NodeInfo,
|
fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient,
|
||||||
subscribe: Boolean, ifChangedSinceVer: Int? = null): ListenableFuture<Unit>
|
subscribe: Boolean, ifChangedSinceVer: Int? = null): ListenableFuture<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -5,6 +5,7 @@ import com.google.common.util.concurrent.SettableFuture
|
|||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -85,13 +86,13 @@ interface WalletService {
|
|||||||
/**
|
/**
|
||||||
* Returns a snapshot of the heads of LinearStates.
|
* Returns a snapshot of the heads of LinearStates.
|
||||||
*/
|
*/
|
||||||
val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||||
|
|
||||||
// TODO: When KT-10399 is fixed, rename this and remove the inline version below.
|
// TODO: When KT-10399 is fixed, rename this and remove the inline version below.
|
||||||
|
|
||||||
/** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */
|
/** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<SecureHash, StateAndRef<T>> {
|
fun <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<UniqueIdentifier, StateAndRef<T>> {
|
||||||
return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState<T>, it.value.ref) }
|
return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState<T>, it.value.ref) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package com.r3corda.core.node.services
|
package com.r3corda.core.node.services
|
||||||
|
|
||||||
import com.r3corda.core.contracts.SignedTransaction
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,26 +1,35 @@
|
|||||||
package com.r3corda.core.protocols
|
package com.r3corda.core.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.node.ServiceHub
|
import com.r3corda.core.node.ServiceHub
|
||||||
import com.r3corda.core.utilities.UntrustworthyData
|
import com.r3corda.core.utilities.UntrustworthyData
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The interface of [ProtocolStateMachineImpl] exposing methods and properties required by ProtocolLogic for compilation.
|
* The interface of [ProtocolStateMachineImpl] exposing methods and properties required by ProtocolLogic for compilation.
|
||||||
*/
|
*/
|
||||||
interface ProtocolStateMachine<R> {
|
interface ProtocolStateMachine<R> {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun <T : Any> sendAndReceive(topic: String, destination: Party, sessionIDForSend: Long, sessionIDForReceive: Long,
|
fun <T : Any> sendAndReceive(topic: String,
|
||||||
payload: Any, recvType: Class<T>): UntrustworthyData<T>
|
destination: Party,
|
||||||
|
sessionIDForSend: Long,
|
||||||
|
sessionIDForReceive: Long,
|
||||||
|
payload: Any,
|
||||||
|
receiveType: Class<T>): UntrustworthyData<T>
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, recvType: Class<T>): UntrustworthyData<T>
|
fun <T : Any> receive(topic: String, sessionIDForReceive: Long, receiveType: Class<T>): UntrustworthyData<T>
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
fun send(topic: String, destination: Party, sessionID: Long, payload: Any)
|
fun send(topic: String, destination: Party, sessionID: Long, payload: Any)
|
||||||
|
|
||||||
val serviceHub: ServiceHub
|
val serviceHub: ServiceHub
|
||||||
val logger: Logger
|
val logger: Logger
|
||||||
|
|
||||||
|
/** Unique ID for this machine, valid only while it is in memory. */
|
||||||
|
val machineId: Long
|
||||||
|
/** This future will complete when the call method returns. */
|
||||||
|
val resultFuture: ListenableFuture<R>
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ import com.r3corda.core.contracts.*
|
|||||||
import com.r3corda.core.crypto.*
|
import com.r3corda.core.crypto.*
|
||||||
import com.r3corda.core.node.AttachmentsClassLoader
|
import com.r3corda.core.node.AttachmentsClassLoader
|
||||||
import com.r3corda.core.node.services.AttachmentStorage
|
import com.r3corda.core.node.services.AttachmentStorage
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import com.r3corda.core.utilities.NonEmptySet
|
import com.r3corda.core.utilities.NonEmptySet
|
||||||
import com.r3corda.core.utilities.NonEmptySetSerializer
|
import com.r3corda.core.utilities.NonEmptySetSerializer
|
||||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||||
@ -232,7 +234,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
|
|||||||
kryo.writeClassAndObject(output, obj.outputs)
|
kryo.writeClassAndObject(output, obj.outputs)
|
||||||
kryo.writeClassAndObject(output, obj.commands)
|
kryo.writeClassAndObject(output, obj.commands)
|
||||||
kryo.writeClassAndObject(output, obj.notary)
|
kryo.writeClassAndObject(output, obj.notary)
|
||||||
kryo.writeClassAndObject(output, obj.signers)
|
kryo.writeClassAndObject(output, obj.mustSign)
|
||||||
kryo.writeClassAndObject(output, obj.type)
|
kryo.writeClassAndObject(output, obj.type)
|
||||||
kryo.writeClassAndObject(output, obj.timestamp)
|
kryo.writeClassAndObject(output, obj.timestamp)
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
package com.r3corda.core.testing
|
|
||||||
|
|
||||||
import com.r3corda.core.contracts.Contract
|
|
||||||
import com.r3corda.core.contracts.LinearState
|
|
||||||
import com.r3corda.core.crypto.SecureHash
|
|
||||||
import java.security.PublicKey
|
|
||||||
|
|
||||||
class DummyLinearState(
|
|
||||||
override val thread: SecureHash = SecureHash.randomSHA256(),
|
|
||||||
override val contract: Contract = AlwaysSucceedContract(),
|
|
||||||
override val participants: List<PublicKey> = listOf(),
|
|
||||||
val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState {
|
|
||||||
|
|
||||||
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
|
|
||||||
return participants.any { ourKeys.contains(it) }
|
|
||||||
}
|
|
||||||
}
|
|
113
core/src/main/kotlin/com/r3corda/core/testing/Generators.kt
Normal file
113
core/src/main/kotlin/com/r3corda/core/testing/Generators.kt
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package com.r3corda.core.testing
|
||||||
|
|
||||||
|
import com.pholser.junit.quickcheck.generator.GenerationStatus
|
||||||
|
import com.pholser.junit.quickcheck.generator.Generator
|
||||||
|
import com.pholser.junit.quickcheck.generator.java.lang.StringGenerator
|
||||||
|
import com.pholser.junit.quickcheck.generator.java.util.ArrayListGenerator
|
||||||
|
import com.pholser.junit.quickcheck.random.SourceOfRandomness
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.*
|
||||||
|
import com.r3corda.core.serialization.OpaqueBytes
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generators for quickcheck
|
||||||
|
*
|
||||||
|
* TODO Split this into several files
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun <A> Generator<A>.generateList(random: SourceOfRandomness, status: GenerationStatus): List<A> {
|
||||||
|
val arrayGenerator = ArrayListGenerator()
|
||||||
|
arrayGenerator.addComponentGenerators(listOf(this))
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return arrayGenerator.generate(random, status) as List<A>
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrivateKeyGenerator: Generator<PrivateKey>(PrivateKey::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PrivateKey {
|
||||||
|
return entropyToKeyPair(random.nextBigInteger(32)).private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PublicKeyGenerator: Generator<PublicKey>(PublicKey::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PublicKey {
|
||||||
|
return entropyToKeyPair(random.nextBigInteger(32)).public
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PartyGenerator: Generator<Party>(Party::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Party {
|
||||||
|
return Party(StringGenerator().generate(random, status), PublicKeyGenerator().generate(random, status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PartyAndReferenceGenerator: Generator<PartyAndReference>(PartyAndReference::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PartyAndReference {
|
||||||
|
return PartyAndReference(PartyGenerator().generate(random, status), OpaqueBytes(random.nextBytes(16)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecureHashGenerator: Generator<SecureHash>(SecureHash::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): SecureHash {
|
||||||
|
return SecureHash.Companion.sha256(random.nextBytes(16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateRefGenerator: Generator<StateRef>(StateRef::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): StateRef {
|
||||||
|
return StateRef(SecureHash.Companion.sha256(random.nextBytes(16)), random.nextInt(0, 10))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
class TransactionStateGenerator<T : ContractState>(val stateGenerator: Generator<T>) : Generator<TransactionState<T>>(TransactionState::class.java as Class<TransactionState<T>>) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): TransactionState<T> {
|
||||||
|
return TransactionState(stateGenerator.generate(random, status), PartyGenerator().generate(random, status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
class IssuedGenerator<T>(val productGenerator: Generator<T>) : Generator<Issued<T>>(Issued::class.java as Class<Issued<T>>) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Issued<T> {
|
||||||
|
return Issued(PartyAndReferenceGenerator().generate(random, status), productGenerator.generate(random, status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
class AmountGenerator<T>(val tokenGenerator: Generator<T>) : Generator<Amount<T>>(Amount::class.java as Class<Amount<T>>) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Amount<T> {
|
||||||
|
return Amount(random.nextLong(0, 1000000), tokenGenerator.generate(random, status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CurrencyGenerator() : Generator<Currency>(Currency::class.java) {
|
||||||
|
companion object {
|
||||||
|
val currencies = Currency.getAvailableCurrencies().toList()
|
||||||
|
}
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Currency {
|
||||||
|
return currencies[random.nextInt(0, currencies.size - 1)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InstantGenerator : Generator<Instant>(Instant::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Instant {
|
||||||
|
return Instant.ofEpochMilli(random.nextLong(0, 1000000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DurationGenerator : Generator<Duration>(Duration::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Duration {
|
||||||
|
return Duration.ofMillis(random.nextLong(0, 1000000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimestampGenerator : Generator<Timestamp>(Timestamp::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Timestamp {
|
||||||
|
return Timestamp(InstantGenerator().generate(random, status), DurationGenerator().generate(random, status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import com.r3corda.core.node.ServiceHub
|
|||||||
import com.r3corda.core.node.services.Wallet
|
import com.r3corda.core.node.services.Wallet
|
||||||
import com.r3corda.core.node.services.WalletService
|
import com.r3corda.core.node.services.WalletService
|
||||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import com.r3corda.core.utilities.loggerFor
|
import com.r3corda.core.utilities.loggerFor
|
||||||
import com.r3corda.core.utilities.trace
|
import com.r3corda.core.utilities.trace
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
@ -22,9 +23,6 @@ import javax.annotation.concurrent.ThreadSafe
|
|||||||
*/
|
*/
|
||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
open class InMemoryWalletService(protected val services: ServiceHub) : SingletonSerializeAsToken(), WalletService {
|
open class InMemoryWalletService(protected val services: ServiceHub) : SingletonSerializeAsToken(), WalletService {
|
||||||
class ClashingThreads(threads: Set<SecureHash>, transactions: Iterable<WireTransaction>) :
|
|
||||||
Exception("There are multiple linear head states after processing transactions $transactions. The clashing thread(s): $threads")
|
|
||||||
|
|
||||||
open protected val log = loggerFor<InMemoryWalletService>()
|
open protected val log = loggerFor<InMemoryWalletService>()
|
||||||
|
|
||||||
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
|
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
|
||||||
@ -46,9 +44,9 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
|
|||||||
/**
|
/**
|
||||||
* Returns a snapshot of the heads of LinearStates.
|
* Returns a snapshot of the heads of LinearStates.
|
||||||
*/
|
*/
|
||||||
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||||
get() = currentWallet.let { wallet ->
|
get() = currentWallet.let { wallet ->
|
||||||
wallet.states.filterStatesOfType<LinearState>().associateBy { it.state.data.thread }.mapValues { it.value }
|
wallet.states.filterStatesOfType<LinearState>().associateBy { it.state.data.linearId }.mapValues { it.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
|
override fun notifyAll(txns: Iterable<WireTransaction>): Wallet {
|
||||||
@ -78,14 +76,6 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
|
|||||||
Pair(wallet, combinedDelta)
|
Pair(wallet, combinedDelta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we need to remove the clashing threads concepts and support potential duplicate threads
|
|
||||||
// because two different nodes can have two different sets of threads and so currently it's possible
|
|
||||||
// for only one party to have a clash which interferes with determinism of the transactions.
|
|
||||||
val clashingThreads = walletAndNetDelta.first.clashingThreads
|
|
||||||
if (!clashingThreads.isEmpty()) {
|
|
||||||
throw ClashingThreads(clashingThreads, txns)
|
|
||||||
}
|
|
||||||
|
|
||||||
wallet = walletAndNetDelta.first
|
wallet = walletAndNetDelta.first
|
||||||
netDelta = walletAndNetDelta.second
|
netDelta = walletAndNetDelta.second
|
||||||
return@locked wallet
|
return@locked wallet
|
||||||
@ -133,23 +123,4 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
|
|||||||
|
|
||||||
return Pair(Wallet(newStates), change)
|
return Pair(Wallet(newStates), change)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
// Returns the set of LinearState threads that clash in the wallet
|
|
||||||
val Wallet.clashingThreads: Set<SecureHash> get() {
|
|
||||||
val clashingThreads = HashSet<SecureHash>()
|
|
||||||
val threadsSeen = HashSet<SecureHash>()
|
|
||||||
for (linearState in states.filterStatesOfType<LinearState>()) {
|
|
||||||
val thread = linearState.state.data.thread
|
|
||||||
if (threadsSeen.contains(thread)) {
|
|
||||||
clashingThreads.add(thread)
|
|
||||||
} else {
|
|
||||||
threadsSeen.add(thread)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clashingThreads
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
package com.r3corda.core.transactions
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract class defining fields shared by all transaction types in the system.
|
||||||
|
*/
|
||||||
|
abstract class BaseTransaction(
|
||||||
|
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
|
||||||
|
open val inputs: List<*>,
|
||||||
|
/** Ordered list of states defined by this transaction, along with the associated notaries. */
|
||||||
|
val outputs: List<TransactionState<ContractState>>,
|
||||||
|
/**
|
||||||
|
* If present, the notary for this transaction. If absent then the transaction is not notarised at all.
|
||||||
|
* This is intended for issuance/genesis transactions that don't consume any other states and thus can't
|
||||||
|
* double spend anything.
|
||||||
|
*/
|
||||||
|
val notary: Party?,
|
||||||
|
/**
|
||||||
|
* Keys that are required to have signed the wrapping [SignedTransaction], ordered to match the list of
|
||||||
|
* signatures. There is nothing that forces the list to be the _correct_ list of signers for this
|
||||||
|
* transaction until the transaction is verified by using [LedgerTransaction.verify]. It includes the
|
||||||
|
* notary key, if the notary field is set.
|
||||||
|
*/
|
||||||
|
val mustSign: List<PublicKey>,
|
||||||
|
/**
|
||||||
|
* Pointer to a class that defines the behaviour of this transaction: either normal, or "notary changing".
|
||||||
|
*/
|
||||||
|
val type: TransactionType,
|
||||||
|
/**
|
||||||
|
* If specified, a time window in which this transaction may have been notarised. Contracts can check this
|
||||||
|
* time window to find out when a transaction is deemed to have occurred, from the ledger's perspective.
|
||||||
|
*/
|
||||||
|
val timestamp: Timestamp?
|
||||||
|
) : NamedByHash {
|
||||||
|
|
||||||
|
protected fun checkInvariants() {
|
||||||
|
if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs." }
|
||||||
|
if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is BaseTransaction &&
|
||||||
|
notary == other.notary &&
|
||||||
|
mustSign == other.mustSign &&
|
||||||
|
type == other.type &&
|
||||||
|
timestamp == other.timestamp
|
||||||
|
|
||||||
|
override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp)
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package com.r3corda.core.transactions
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||||
|
*
|
||||||
|
* - Downloading and locally storing all the dependencies of the transaction.
|
||||||
|
* - Resolving the input states and loading them into memory.
|
||||||
|
* - Doing some basic key lookups on the [Command]s to see if any keys are from a recognised party, thus converting the
|
||||||
|
* [Command] objects into [AuthenticatedObject].
|
||||||
|
* - Deserialising the output states.
|
||||||
|
*
|
||||||
|
* All the above refer to inputs using a (txhash, output index) pair.
|
||||||
|
*/
|
||||||
|
class LedgerTransaction(
|
||||||
|
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
|
||||||
|
override val inputs: List<StateAndRef<*>>,
|
||||||
|
outputs: List<TransactionState<ContractState>>,
|
||||||
|
/** Arbitrary data passed to the program of each input state. */
|
||||||
|
val commands: List<AuthenticatedObject<CommandData>>,
|
||||||
|
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
|
||||||
|
val attachments: List<Attachment>,
|
||||||
|
/** The hash of the original serialised WireTransaction. */
|
||||||
|
override val id: SecureHash,
|
||||||
|
notary: Party?,
|
||||||
|
signers: List<PublicKey>,
|
||||||
|
timestamp: Timestamp?,
|
||||||
|
type: TransactionType
|
||||||
|
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
|
||||||
|
init { checkInvariants() }
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
|
||||||
|
|
||||||
|
// TODO: Remove this concept.
|
||||||
|
// There isn't really a good justification for hiding this data from the contract, it's just a backwards compat hack.
|
||||||
|
/** Strips the transaction down to a form that is usable by the contract verify functions */
|
||||||
|
fun toTransactionForContract(): TransactionForContract {
|
||||||
|
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
|
||||||
|
inputs.map { it.state.notary }.singleOrNull(), timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies this transaction and throws an exception if not valid, depending on the type. For general transactions:
|
||||||
|
*
|
||||||
|
* - The contracts are run with the transaction as the input.
|
||||||
|
* - The list of keys mentioned in commands is compared against the signers list.
|
||||||
|
*
|
||||||
|
* @throws TransactionVerificationException if anything goes wrong.
|
||||||
|
*/
|
||||||
|
fun verify() = type.verify(this)
|
||||||
|
|
||||||
|
// TODO: When we upgrade to Kotlin 1.1 we can make this a data class again and have the compiler generate these.
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other?.javaClass != javaClass) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as LedgerTransaction
|
||||||
|
|
||||||
|
if (inputs != other.inputs) return false
|
||||||
|
if (outputs != other.outputs) return false
|
||||||
|
if (commands != other.commands) return false
|
||||||
|
if (attachments != other.attachments) return false
|
||||||
|
if (id != other.id) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + inputs.hashCode()
|
||||||
|
result = 31 * result + outputs.hashCode()
|
||||||
|
result = 31 * result + commands.hashCode()
|
||||||
|
result = 31 * result + attachments.hashCode()
|
||||||
|
result = 31 * result + id.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
package com.r3corda.core.transactions
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.NamedByHash
|
||||||
|
import com.r3corda.core.contracts.TransactionResolutionException
|
||||||
|
import com.r3corda.core.crypto.DigitalSignature
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.crypto.toStringsShort
|
||||||
|
import com.r3corda.core.node.ServiceHub
|
||||||
|
import com.r3corda.core.serialization.SerializedBytes
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.SignatureException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
|
||||||
|
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
|
||||||
|
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash
|
||||||
|
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs may
|
||||||
|
* map to the same key (and they could be different in important ways, like validity!). The signatures on a
|
||||||
|
* SignedTransaction might be invalid or missing: the type does not imply validity.
|
||||||
|
*/
|
||||||
|
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||||
|
val sigs: List<DigitalSignature.WithKey>) : NamedByHash {
|
||||||
|
init {
|
||||||
|
require(sigs.isNotEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This needs to be reworked to ensure that the inner WireTransaction is only ever deserialised sandboxed.
|
||||||
|
|
||||||
|
/** Lazily calculated access to the deserialised/hashed transaction data. */
|
||||||
|
val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) }
|
||||||
|
|
||||||
|
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
|
||||||
|
override val id: SecureHash get() = txBits.hash
|
||||||
|
|
||||||
|
class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash) : NamedByHash, SignatureException() {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.toStringsShort()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the signatures on this transaction and throws if any are missing which aren't passed as parameters.
|
||||||
|
* In this context, "verifying" means checking they are valid signatures and that their public keys are in
|
||||||
|
* the contained transactions [BaseTransaction.mustSign] property.
|
||||||
|
*
|
||||||
|
* Normally you would not provide any keys to this function, but if you're in the process of building a partial
|
||||||
|
* transaction and you want to access the contents before you've signed it, you can specify your own keys here
|
||||||
|
* to bypass that check.
|
||||||
|
*
|
||||||
|
* @throws SignatureException if any signatures are invalid or unrecognised.
|
||||||
|
* @throws SignaturesMissingException if any signatures should have been present but were not.
|
||||||
|
*/
|
||||||
|
@Throws(SignatureException::class)
|
||||||
|
fun verifySignatures(vararg allowedToBeMissing: PublicKey): WireTransaction {
|
||||||
|
// Embedded WireTransaction is not deserialised until after we check the signatures.
|
||||||
|
checkSignaturesAreValid()
|
||||||
|
|
||||||
|
val missing = getMissingSignatures()
|
||||||
|
if (missing.isNotEmpty()) {
|
||||||
|
val allowed = setOf(*allowedToBeMissing)
|
||||||
|
val needed = missing - allowed
|
||||||
|
if (needed.isNotEmpty())
|
||||||
|
throw SignaturesMissingException(needed, getMissingKeyDescriptions(needed), id)
|
||||||
|
}
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mathematically validates the signatures that are present on this transaction. This does not imply that
|
||||||
|
* the signatures are by the right keys, or that there are sufficient signatures, just that they aren't
|
||||||
|
* corrupt. If you use this function directly you'll need to do the other checks yourself. Probably you
|
||||||
|
* want [verifySignatures] instead.
|
||||||
|
*
|
||||||
|
* @throws SignatureException if a signature fails to verify.
|
||||||
|
*/
|
||||||
|
@Throws(SignatureException::class)
|
||||||
|
fun checkSignaturesAreValid() {
|
||||||
|
for (sig in sigs)
|
||||||
|
sig.verifyWithECDSA(txBits.bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMissingSignatures(): Set<PublicKey> {
|
||||||
|
val requiredKeys = tx.mustSign.toSet()
|
||||||
|
val sigKeys = sigs.map { it.by }.toSet()
|
||||||
|
|
||||||
|
if (sigKeys.containsAll(requiredKeys)) return emptySet()
|
||||||
|
return requiredKeys - sigKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a human readable description of where signatures are required from, and are missing, to assist in debugging
|
||||||
|
* the underlying cause.
|
||||||
|
*/
|
||||||
|
private fun getMissingKeyDescriptions(missing: Set<PublicKey>): ArrayList<String> {
|
||||||
|
// TODO: We need a much better way of structuring this data
|
||||||
|
val missingElements = ArrayList<String>()
|
||||||
|
this.tx.commands.forEach { command ->
|
||||||
|
if (command.signers.any { it in missing })
|
||||||
|
missingElements.add(command.toString())
|
||||||
|
}
|
||||||
|
if (this.tx.notary?.owningKey in missing)
|
||||||
|
missingElements.add("notary")
|
||||||
|
return missingElements
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the same transaction but with an additional (unchecked) signature. */
|
||||||
|
fun withAdditionalSignature(sig: DigitalSignature.WithKey) = copy(sigs = sigs + sig)
|
||||||
|
/** Returns the same transaction but with an additional (unchecked) signatures. */
|
||||||
|
fun withAdditionalSignatures(sigList: Iterable<DigitalSignature.WithKey>) = copy(sigs = sigs + sigList)
|
||||||
|
|
||||||
|
/** Alias for [withAdditionalSignature] to let you use Kotlin operator overloading. */
|
||||||
|
operator fun plus(sig: DigitalSignature.WithKey) = withAdditionalSignature(sig)
|
||||||
|
/** Alias for [withAdditionalSignatures] to let you use Kotlin operator overloading. */
|
||||||
|
operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls [verifySignatures] to check all required signatures are present, and then calls
|
||||||
|
* [WireTransaction.toLedgerTransaction] with the passed in [ServiceHub] to resolve the dependencies,
|
||||||
|
* returning an unverified LedgerTransaction.
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException if a required attachment was not found in storage.
|
||||||
|
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
||||||
|
* @throws SignatureException if any signatures were invalid or unrecognised
|
||||||
|
* @throws SignaturesMissingException if any signatures that should have been present are missing.
|
||||||
|
*/
|
||||||
|
@Throws(FileNotFoundException::class, TransactionResolutionException::class, SignaturesMissingException::class)
|
||||||
|
fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services)
|
||||||
|
}
|
@ -1,7 +1,10 @@
|
|||||||
package com.r3corda.core.contracts
|
package com.r3corda.core.transactions
|
||||||
|
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
import com.r3corda.core.crypto.*
|
import com.r3corda.core.crypto.*
|
||||||
import com.r3corda.core.serialization.serialize
|
import com.r3corda.core.serialization.serialize
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@ -19,6 +22,10 @@ import java.util.*
|
|||||||
* @param notary Notary used for the transaction. If null, this indicates the transaction DOES NOT have a notary.
|
* @param notary Notary used for the transaction. If null, this indicates the transaction DOES NOT have a notary.
|
||||||
* When this is set to a non-null value, an output state can be added by just passing in a [ContractState] – a
|
* When this is set to a non-null value, an output state can be added by just passing in a [ContractState] – a
|
||||||
* [TransactionState] with this notary specified will be generated automatically.
|
* [TransactionState] with this notary specified will be generated automatically.
|
||||||
|
*
|
||||||
|
* @param signers The set of public keys the transaction needs signatures for. The logic for building the signers set
|
||||||
|
* can be customised for every [TransactionType]. E.g. in the general case it contains the command and notary public keys,
|
||||||
|
* but for the [TransactionType.NotaryChange] transactions it is the set of all input [ContractState.participants].
|
||||||
*/
|
*/
|
||||||
open class TransactionBuilder(
|
open class TransactionBuilder(
|
||||||
protected val type: TransactionType = TransactionType.General(),
|
protected val type: TransactionType = TransactionType.General(),
|
||||||
@ -30,7 +37,6 @@ open class TransactionBuilder(
|
|||||||
protected val signers: MutableSet<PublicKey> = mutableSetOf(),
|
protected val signers: MutableSet<PublicKey> = mutableSetOf(),
|
||||||
protected var timestamp: Timestamp? = null) {
|
protected var timestamp: Timestamp? = null) {
|
||||||
|
|
||||||
@Deprecated("use timestamp instead")
|
|
||||||
val time: Timestamp? get() = timestamp
|
val time: Timestamp? get() = timestamp
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -91,10 +97,11 @@ open class TransactionBuilder(
|
|||||||
/** The signatures that have been collected so far - might be incomplete! */
|
/** The signatures that have been collected so far - might be incomplete! */
|
||||||
protected val currentSigs = arrayListOf<DigitalSignature.WithKey>()
|
protected val currentSigs = arrayListOf<DigitalSignature.WithKey>()
|
||||||
|
|
||||||
fun signWith(key: KeyPair) {
|
fun signWith(key: KeyPair): TransactionBuilder {
|
||||||
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
|
||||||
val data = toWireTransaction().serialize()
|
val data = toWireTransaction().serialize()
|
||||||
addSignatureUnchecked(key.signWithECDSA(data.bits))
|
addSignatureUnchecked(key.signWithECDSA(data.bits))
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,13 +146,12 @@ open class TransactionBuilder(
|
|||||||
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun addInputState(stateAndRef: StateAndRef<*>) = addInputState(stateAndRef.ref, stateAndRef.state.notary)
|
open fun addInputState(stateAndRef: StateAndRef<*>) {
|
||||||
|
|
||||||
fun addInputState(stateRef: StateRef, notary: Party) {
|
|
||||||
check(currentSigs.isEmpty())
|
check(currentSigs.isEmpty())
|
||||||
|
val notary = stateAndRef.state.notary
|
||||||
require(notary == this.notary) { "Input state requires notary \"${notary}\" which does not match the transaction notary \"${this.notary}\"." }
|
require(notary == this.notary) { "Input state requires notary \"${notary}\" which does not match the transaction notary \"${this.notary}\"." }
|
||||||
signers.add(notary.owningKey)
|
signers.add(notary.owningKey)
|
||||||
inputs.add(stateRef)
|
inputs.add(stateAndRef.ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAttachment(attachmentId: SecureHash) {
|
fun addAttachment(attachmentId: SecureHash) {
|
@ -0,0 +1,118 @@
|
|||||||
|
package com.r3corda.core.transactions
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.Kryo
|
||||||
|
import com.r3corda.core.contracts.*
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
import com.r3corda.core.indexOfOrThrow
|
||||||
|
import com.r3corda.core.node.ServiceHub
|
||||||
|
import com.r3corda.core.serialization.SerializedBytes
|
||||||
|
import com.r3corda.core.serialization.THREAD_LOCAL_KRYO
|
||||||
|
import com.r3corda.core.serialization.deserialize
|
||||||
|
import com.r3corda.core.serialization.serialize
|
||||||
|
import com.r3corda.core.utilities.Emoji
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A transaction ready for serialisation, without any signatures attached. A WireTransaction is usually wrapped
|
||||||
|
* by a [SignedTransaction] that carries the signatures over this payload. The hash of the wire transaction is
|
||||||
|
* the identity of the transaction, that is, it's possible for two [SignedTransaction]s with different sets of
|
||||||
|
* signatures to have the same identity hash.
|
||||||
|
*/
|
||||||
|
class WireTransaction(
|
||||||
|
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
|
||||||
|
override val inputs: List<StateRef>,
|
||||||
|
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
|
||||||
|
val attachments: List<SecureHash>,
|
||||||
|
outputs: List<TransactionState<ContractState>>,
|
||||||
|
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
|
||||||
|
val commands: List<Command>,
|
||||||
|
notary: Party?,
|
||||||
|
signers: List<PublicKey>,
|
||||||
|
type: TransactionType,
|
||||||
|
timestamp: Timestamp?
|
||||||
|
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
|
||||||
|
init { checkInvariants() }
|
||||||
|
|
||||||
|
// Cache the serialised form of the transaction and its hash to give us fast access to it.
|
||||||
|
@Volatile @Transient private var cachedBits: SerializedBytes<WireTransaction>? = null
|
||||||
|
val serialized: SerializedBytes<WireTransaction> get() = cachedBits ?: serialize().apply { cachedBits = this }
|
||||||
|
override val id: SecureHash get() = serialized.hash
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun deserialize(bits: SerializedBytes<WireTransaction>, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction {
|
||||||
|
val wtx = bits.bits.deserialize<WireTransaction>(kryo)
|
||||||
|
wtx.cachedBits = bits
|
||||||
|
return wtx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a [StateAndRef] for the given output index. */
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T : ContractState> outRef(index: Int): StateAndRef<T> {
|
||||||
|
require(index >= 0 && index < outputs.size)
|
||||||
|
return StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a [StateAndRef] for the requested output state, or throws [IllegalArgumentException] if not found. */
|
||||||
|
fun <T : ContractState> outRef(state: ContractState): StateAndRef<T> = outRef(outputs.map { it.data }.indexOfOrThrow(state))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
|
||||||
|
* have been fully resolved using the resolution protocol by this point.
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException if a required attachment was not found in storage.
|
||||||
|
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
||||||
|
*/
|
||||||
|
@Throws(FileNotFoundException::class, TransactionResolutionException::class)
|
||||||
|
fun toLedgerTransaction(services: ServiceHub): LedgerTransaction {
|
||||||
|
// Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future.
|
||||||
|
val authenticatedArgs = commands.map {
|
||||||
|
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
|
||||||
|
AuthenticatedObject(it.signers, parties, it.value)
|
||||||
|
}
|
||||||
|
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
|
||||||
|
val attachments = attachments.map {
|
||||||
|
services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString())
|
||||||
|
}
|
||||||
|
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) }
|
||||||
|
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timestamp, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
val buf = StringBuilder()
|
||||||
|
buf.appendln("Transaction $id:")
|
||||||
|
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
|
||||||
|
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
|
||||||
|
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
|
||||||
|
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
|
||||||
|
return buf.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: When Kotlin 1.1 comes out we can make this class a data class again, and have these be autogenerated.
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other?.javaClass != javaClass) return false
|
||||||
|
if (!super.equals(other)) return false
|
||||||
|
|
||||||
|
other as WireTransaction
|
||||||
|
|
||||||
|
if (inputs != other.inputs) return false
|
||||||
|
if (attachments != other.attachments) return false
|
||||||
|
if (outputs != other.outputs) return false
|
||||||
|
if (commands != other.commands) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = super.hashCode()
|
||||||
|
result = 31 * result + inputs.hashCode()
|
||||||
|
result = 31 * result + attachments.hashCode()
|
||||||
|
result = 31 * result + outputs.hashCode()
|
||||||
|
result = 31 * result + commands.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,8 @@ import kotlin.reflect.KClass
|
|||||||
// logging at that level is enabled.
|
// logging at that level is enabled.
|
||||||
inline fun <reified T : Any> loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java)
|
inline fun <reified T : Any> loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java)
|
||||||
|
|
||||||
inline fun org.slf4j.Logger.trace(msg: () -> String) {
|
inline fun org.slf4j.Logger.trace(msg: () -> String) { if (isTraceEnabled) trace(msg()) }
|
||||||
if (isTraceEnabled) trace(msg())
|
inline fun org.slf4j.Logger.debug(msg: () -> String) { if (isDebugEnabled) debug(msg()) }
|
||||||
}
|
|
||||||
|
|
||||||
/** A configuration helper that allows modifying the log level for specific loggers */
|
/** A configuration helper that allows modifying the log level for specific loggers */
|
||||||
object LogHelper {
|
object LogHelper {
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
@file:JvmName("TestConstants")
|
||||||
|
package com.r3corda.core.utilities
|
||||||
|
|
||||||
|
import com.r3corda.core.crypto.*
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
// A dummy time at which we will be pretending test transactions are created.
|
||||||
|
val TEST_TX_TIME: Instant get() = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||||
|
|
||||||
|
val DUMMY_PUBKEY_1: PublicKey get() = DummyPublicKey("x1")
|
||||||
|
val DUMMY_PUBKEY_2: PublicKey get() = DummyPublicKey("x2")
|
||||||
|
|
||||||
|
val DUMMY_KEY_1: KeyPair by lazy { generateKeyPair() }
|
||||||
|
val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() }
|
||||||
|
|
||||||
|
val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
|
||||||
|
val DUMMY_NOTARY: Party get() = Party("Notary Service", DUMMY_NOTARY_KEY.public)
|
@ -17,5 +17,9 @@ class UntrustworthyData<out T>(private val fromUntrustedWorld: T) {
|
|||||||
get() = fromUntrustedWorld
|
get() = fromUntrustedWorld
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
inline fun <R> unwrap(validator: (T) -> R) = validator(data)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("This old name was confusing, use unwrap instead", replaceWith = ReplaceWith("unwrap"))
|
||||||
inline fun <R> validate(validator: (T) -> R) = validator(data)
|
inline fun <R> validate(validator: (T) -> R) = validator(data)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package com.r3corda.protocols
|
package com.r3corda.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.ContractState
|
||||||
|
import com.r3corda.core.contracts.StateAndRef
|
||||||
|
import com.r3corda.core.contracts.StateRef
|
||||||
import com.r3corda.core.crypto.DigitalSignature
|
import com.r3corda.core.crypto.DigitalSignature
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.signWithECDSA
|
import com.r3corda.core.crypto.signWithECDSA
|
||||||
@ -9,7 +11,10 @@ import com.r3corda.core.messaging.Ack
|
|||||||
import com.r3corda.core.node.NodeInfo
|
import com.r3corda.core.node.NodeInfo
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
import com.r3corda.core.random63BitValue
|
import com.r3corda.core.random63BitValue
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
import com.r3corda.core.utilities.ProgressTracker
|
||||||
|
import com.r3corda.core.utilities.UntrustworthyData
|
||||||
import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
|
import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
|
||||||
import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
|
import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -98,7 +103,7 @@ abstract class AbstractStateReplacementProtocol<T> {
|
|||||||
sendAndReceive<Ack>(node.identity, 0, sessionIdForReceive, handshake)
|
sendAndReceive<Ack>(node.identity, 0, sessionIdForReceive, handshake)
|
||||||
|
|
||||||
val response = sendAndReceive<Result>(node.identity, sessionIdForSend, sessionIdForReceive, proposal)
|
val response = sendAndReceive<Result>(node.identity, sessionIdForSend, sessionIdForReceive, proposal)
|
||||||
val participantSignature = response.validate {
|
val participantSignature = response.unwrap {
|
||||||
if (it.sig == null) throw StateReplacementException(it.error!!)
|
if (it.sig == null) throw StateReplacementException(it.error!!)
|
||||||
else {
|
else {
|
||||||
check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" }
|
check(it.sig.by == node.identity.owningKey) { "Not signed by the required participant" }
|
||||||
@ -117,7 +122,7 @@ abstract class AbstractStateReplacementProtocol<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Acceptor<in T>(val otherSide: Party,
|
abstract class Acceptor<T>(val otherSide: Party,
|
||||||
val sessionIdForSend: Long,
|
val sessionIdForSend: Long,
|
||||||
val sessionIdForReceive: Long,
|
val sessionIdForReceive: Long,
|
||||||
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() {
|
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() {
|
||||||
@ -135,24 +140,21 @@ abstract class AbstractStateReplacementProtocol<T> {
|
|||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
val proposal = receive<Proposal<T>>(sessionIdForReceive).validate { it }
|
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(sessionIdForReceive)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
verifyProposal(proposal)
|
val stx: SignedTransaction = maybeProposal.unwrap { verifyProposal(maybeProposal).stx }
|
||||||
verifyTx(proposal.stx)
|
verifyTx(stx)
|
||||||
|
approve(stx)
|
||||||
} catch(e: Exception) {
|
} catch(e: Exception) {
|
||||||
// TODO: catch only specific exceptions. However, there are numerous validation exceptions
|
// TODO: catch only specific exceptions. However, there are numerous validation exceptions
|
||||||
// that might occur (tx validation/resolution, invalid proposal). Need to rethink how
|
// that might occur (tx validation/resolution, invalid proposal). Need to rethink how
|
||||||
// we manage exceptions and maybe introduce some platform exception hierarchy
|
// we manage exceptions and maybe introduce some platform exception hierarchy
|
||||||
val myIdentity = serviceHub.storageService.myLegalIdentity
|
val myIdentity = serviceHub.storageService.myLegalIdentity
|
||||||
val state = proposal.stateRef
|
val state = maybeProposal.unwrap { it.stateRef }
|
||||||
val reason = StateReplacementRefused(myIdentity, state, e.message)
|
val reason = StateReplacementRefused(myIdentity, state, e.message)
|
||||||
|
|
||||||
reject(reason)
|
reject(reason)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
approve(proposal.stx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@ -164,7 +166,7 @@ abstract class AbstractStateReplacementProtocol<T> {
|
|||||||
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response)
|
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response)
|
||||||
|
|
||||||
// TODO: This step should not be necessary, as signatures are re-checked in verifySignatures.
|
// TODO: This step should not be necessary, as signatures are re-checked in verifySignatures.
|
||||||
val allSignatures = swapSignatures.validate { signatures ->
|
val allSignatures = swapSignatures.unwrap { signatures ->
|
||||||
signatures.forEach { it.verifyWithECDSA(stx.txBits) }
|
signatures.forEach { it.verifyWithECDSA(stx.txBits) }
|
||||||
signatures
|
signatures
|
||||||
}
|
}
|
||||||
@ -183,9 +185,10 @@ abstract class AbstractStateReplacementProtocol<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the state change proposal to confirm that it's acceptable to this node. Rules for verification depend
|
* Check the state change proposal to confirm that it's acceptable to this node. Rules for verification depend
|
||||||
* on the change proposed, and may further depend on the node itself (for example configuration).
|
* on the change proposed, and may further depend on the node itself (for example configuration). The
|
||||||
|
* proposal is returned if acceptable, otherwise an exception is thrown.
|
||||||
*/
|
*/
|
||||||
abstract internal fun verifyProposal(proposal: Proposal<T>)
|
abstract fun verifyProposal(maybeProposal: UntrustworthyData<Proposal<T>>): Proposal<T>
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun verifyTx(stx: SignedTransaction) {
|
private fun verifyTx(stx: SignedTransaction) {
|
||||||
@ -199,7 +202,7 @@ abstract class AbstractStateReplacementProtocol<T> {
|
|||||||
private fun checkMySignatureRequired(tx: WireTransaction) {
|
private fun checkMySignatureRequired(tx: WireTransaction) {
|
||||||
// TODO: use keys from the keyManagementService instead
|
// TODO: use keys from the keyManagementService instead
|
||||||
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
|
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
|
||||||
require(tx.signers.contains(myKey)) { "Party is not a participant for any of the input states of transaction ${tx.id}" }
|
require(myKey in tx.mustSign) { "Party is not a participant for any of the input states of transaction ${tx.id}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
package com.r3corda.protocols
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
|
import com.r3corda.core.random63BitValue
|
||||||
|
import com.r3corda.core.serialization.serialize
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify all involved parties about a transaction, including storing a copy. Normally this would be called via
|
||||||
|
* [FinalityProtocol].
|
||||||
|
*
|
||||||
|
* @param notarisedTransaction transaction which has been notarised (if needed) and is ready to notify nodes about.
|
||||||
|
* @param events information on the event(s) which triggered the transaction.
|
||||||
|
* @param participants a list of participants involved in the transaction.
|
||||||
|
* @return a list of participants who were successfully notified of the transaction.
|
||||||
|
*/
|
||||||
|
// TODO: Event needs to be replaced with something that's meaningful, but won't ever contain sensitive
|
||||||
|
// information (such as internal details of an account to take payment from). Suggest
|
||||||
|
// splitting ClientToServiceCommand into public and private parts, with only the public parts
|
||||||
|
// relayed here.
|
||||||
|
class BroadcastTransactionProtocol(val notarisedTransaction: SignedTransaction,
|
||||||
|
val events: Set<ClientToServiceCommand>,
|
||||||
|
val participants: Set<Party>) : ProtocolLogic<Unit>() {
|
||||||
|
companion object {
|
||||||
|
/** Topic for messages notifying a node of a new transaction */
|
||||||
|
val TOPIC = "platform.wallet.notify_tx"
|
||||||
|
}
|
||||||
|
|
||||||
|
override val topic: String = TOPIC
|
||||||
|
|
||||||
|
data class NotifyTxRequestMessage(
|
||||||
|
val tx: SignedTransaction,
|
||||||
|
val events: Set<ClientToServiceCommand>,
|
||||||
|
override val replyToParty: Party,
|
||||||
|
override val sessionID: Long
|
||||||
|
) : PartyRequestMessage
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
// Record it locally
|
||||||
|
serviceHub.recordTransactions(notarisedTransaction)
|
||||||
|
|
||||||
|
// TODO: Messaging layer should handle this broadcast for us (although we need to not be sending
|
||||||
|
// session ID, for that to work, as well).
|
||||||
|
participants.filter { it != serviceHub.storageService.myLegalIdentity }.forEach { participant ->
|
||||||
|
val sessionID = random63BitValue()
|
||||||
|
val msg = NotifyTxRequestMessage(notarisedTransaction, events, serviceHub.storageService.myLegalIdentity, sessionID)
|
||||||
|
send(participant, 0, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -81,7 +81,7 @@ abstract class FetchDataProtocol<T : NamedByHash, in W : Any>(
|
|||||||
|
|
||||||
private fun validateFetchResponse(maybeItems: UntrustworthyData<ArrayList<W?>>,
|
private fun validateFetchResponse(maybeItems: UntrustworthyData<ArrayList<W?>>,
|
||||||
requests: List<SecureHash>): List<T> =
|
requests: List<SecureHash>): List<T> =
|
||||||
maybeItems.validate { response ->
|
maybeItems.unwrap { response ->
|
||||||
if (response.size != requests.size)
|
if (response.size != requests.size)
|
||||||
throw BadAnswer()
|
throw BadAnswer()
|
||||||
for ((index, resp) in response.withIndex()) {
|
for ((index, resp) in response.withIndex()) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package com.r3corda.protocols
|
package com.r3corda.protocols
|
||||||
|
|
||||||
import com.r3corda.core.contracts.SignedTransaction
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
|
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
package com.r3corda.protocols
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||||
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.utilities.ProgressTracker
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finalise a transaction by notarising it, then recording it locally, and then sending it to all involved parties.
|
||||||
|
*
|
||||||
|
* @param transaction to commit.
|
||||||
|
* @param events information on the event(s) which triggered the transaction.
|
||||||
|
* @param participants a list of participants involved in the transaction.
|
||||||
|
* @return a list of participants who were successfully notified of the transaction.
|
||||||
|
*/
|
||||||
|
// TODO: Event needs to be replaced with something that's meaningful, but won't ever contain sensitive
|
||||||
|
// information (such as internal details of an account to take payment from). Suggest
|
||||||
|
// splitting ClientToServiceCommand into public and private parts, with only the public parts
|
||||||
|
// relayed here.
|
||||||
|
class FinalityProtocol(val transaction: SignedTransaction,
|
||||||
|
val events: Set<ClientToServiceCommand>,
|
||||||
|
val participants: Set<Party>,
|
||||||
|
override val progressTracker: ProgressTracker = tracker()): ProtocolLogic<Unit>() {
|
||||||
|
companion object {
|
||||||
|
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service")
|
||||||
|
object BROADCASTING : ProgressTracker.Step("Broadcasting transaction to participants")
|
||||||
|
|
||||||
|
fun tracker() = ProgressTracker(NOTARISING, BROADCASTING)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val topic: String
|
||||||
|
get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
// TODO: Resolve the tx here: it's probably already been done, but re-resolution is a no-op and it'll make the API more forgiving.
|
||||||
|
|
||||||
|
progressTracker.currentStep = NOTARISING
|
||||||
|
// Notarise the transaction if needed
|
||||||
|
val notarisedTransaction = if (needsNotarySignature(transaction)) {
|
||||||
|
val notarySig = subProtocol(NotaryProtocol.Client(transaction))
|
||||||
|
transaction.withAdditionalSignature(notarySig)
|
||||||
|
} else {
|
||||||
|
transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let everyone else know about the transaction
|
||||||
|
progressTracker.currentStep = BROADCASTING
|
||||||
|
subProtocol(BroadcastTransactionProtocol(notarisedTransaction, events, participants))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun needsNotarySignature(stx: SignedTransaction) = stx.tx.notary != null && hasNoNotarySignature(stx)
|
||||||
|
private fun hasNoNotarySignature(stx: SignedTransaction) = stx.tx.notary?.owningKey !in stx.sigs.map { it.by }
|
||||||
|
}
|
@ -1,9 +1,16 @@
|
|||||||
package com.r3corda.protocols
|
package com.r3corda.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.r3corda.core.contracts.*
|
import com.r3corda.core.contracts.ContractState
|
||||||
|
import com.r3corda.core.contracts.StateAndRef
|
||||||
|
import com.r3corda.core.contracts.StateRef
|
||||||
|
import com.r3corda.core.contracts.TransactionType
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
import com.r3corda.core.utilities.ProgressTracker
|
import com.r3corda.core.utilities.ProgressTracker
|
||||||
|
import com.r3corda.core.utilities.UntrustworthyData
|
||||||
|
import com.r3corda.protocols.NotaryChangeProtocol.Acceptor
|
||||||
|
import com.r3corda.protocols.NotaryChangeProtocol.Instigator
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,18 +68,25 @@ object NotaryChangeProtocol: AbstractStateReplacementProtocol<Party>() {
|
|||||||
* TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal
|
* TODO: In more difficult cases this should call for human attention to manually verify and approve the proposal
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun verifyProposal(proposal: AbstractStateReplacementProtocol.Proposal<Party>) {
|
override fun verifyProposal(maybeProposal: UntrustworthyData<AbstractStateReplacementProtocol.Proposal<Party>>): AbstractStateReplacementProtocol.Proposal<Party> {
|
||||||
val newNotary = proposal.modification
|
return maybeProposal.unwrap { proposal ->
|
||||||
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary }
|
val newNotary = proposal.modification
|
||||||
require(isNotary) { "The proposed node $newNotary does not run a Notary service " }
|
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary }
|
||||||
|
require(isNotary) { "The proposed node $newNotary does not run a Notary service " }
|
||||||
|
|
||||||
val state = proposal.stateRef
|
val state = proposal.stateRef
|
||||||
val proposedTx = proposal.stx.tx
|
val proposedTx = proposal.stx.tx
|
||||||
require(proposedTx.inputs.contains(state)) { "The proposed state $state is not in the proposed transaction inputs" }
|
require(state in proposedTx.inputs) { "The proposed state $state is not in the proposed transaction inputs" }
|
||||||
|
require(proposedTx.type.javaClass == TransactionType.NotaryChange::class.java) {
|
||||||
|
"The proposed transaction is not a notary change transaction."
|
||||||
|
}
|
||||||
|
|
||||||
// An example requirement
|
// An example requirement
|
||||||
val blacklist = listOf("Evil Notary")
|
val blacklist = listOf("Evil Notary")
|
||||||
require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" }
|
require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" }
|
||||||
|
|
||||||
|
proposal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
package com.r3corda.protocols
|
package com.r3corda.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.r3corda.core.contracts.SignedTransaction
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
import com.r3corda.core.contracts.StateRef
|
import com.r3corda.core.contracts.StateRef
|
||||||
import com.r3corda.core.contracts.Timestamp
|
import com.r3corda.core.contracts.Timestamp
|
||||||
import com.r3corda.core.contracts.WireTransaction
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import com.r3corda.core.crypto.DigitalSignature
|
import com.r3corda.core.crypto.DigitalSignature
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SignedData
|
import com.r3corda.core.crypto.SignedData
|
||||||
@ -72,7 +72,7 @@ object NotaryProtocol {
|
|||||||
private fun validateResponse(response: UntrustworthyData<Result>): Result {
|
private fun validateResponse(response: UntrustworthyData<Result>): Result {
|
||||||
progressTracker.currentStep = VALIDATING
|
progressTracker.currentStep = VALIDATING
|
||||||
|
|
||||||
response.validate {
|
response.unwrap {
|
||||||
if (it.sig != null) validateSignature(it.sig, stx.txBits)
|
if (it.sig != null) validateSignature(it.sig, stx.txBits)
|
||||||
else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
|
else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
|
||||||
else if (it.error == null || it.error !is NotaryError)
|
else if (it.error == null || it.error !is NotaryError)
|
||||||
@ -105,7 +105,7 @@ object NotaryProtocol {
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call() {
|
||||||
val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).validate { it }
|
val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).unwrap { it }
|
||||||
val wtx = stx.tx
|
val wtx = stx.tx
|
||||||
|
|
||||||
val result = try {
|
val result = try {
|
||||||
@ -205,5 +205,5 @@ sealed class NotaryError {
|
|||||||
|
|
||||||
class TransactionInvalid : NotaryError()
|
class TransactionInvalid : NotaryError()
|
||||||
|
|
||||||
class SignaturesMissing(val missingSigners: List<PublicKey>) : NotaryError()
|
class SignaturesMissing(val missingSigners: Set<PublicKey>) : NotaryError()
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ package com.r3corda.protocols
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.r3corda.core.contracts.Fix
|
import com.r3corda.core.contracts.Fix
|
||||||
import com.r3corda.core.contracts.FixOf
|
import com.r3corda.core.contracts.FixOf
|
||||||
import com.r3corda.core.contracts.TransactionBuilder
|
import com.r3corda.core.transactions.TransactionBuilder
|
||||||
import com.r3corda.core.contracts.WireTransaction
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import com.r3corda.core.crypto.DigitalSignature
|
import com.r3corda.core.crypto.DigitalSignature
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
@ -85,7 +85,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
|
|||||||
val req = SignRequest(wtx, serviceHub.storageService.myLegalIdentity, sessionID)
|
val req = SignRequest(wtx, serviceHub.storageService.myLegalIdentity, sessionID)
|
||||||
val resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(oracle, 0, sessionID, req)
|
val resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(oracle, 0, sessionID, req)
|
||||||
|
|
||||||
return resp.validate { sig ->
|
return resp.unwrap { sig ->
|
||||||
check(sig.signer == oracle)
|
check(sig.signer == oracle)
|
||||||
tx.checkSignature(sig)
|
tx.checkSignature(sig)
|
||||||
sig
|
sig
|
||||||
@ -100,7 +100,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
|
|||||||
// TODO: add deadline to receive
|
// TODO: add deadline to receive
|
||||||
val resp = sendAndReceive<ArrayList<Fix>>(oracle, 0, sessionID, req)
|
val resp = sendAndReceive<ArrayList<Fix>>(oracle, 0, sessionID, req)
|
||||||
|
|
||||||
return resp.validate {
|
return resp.unwrap {
|
||||||
val fix = it.first()
|
val fix = it.first()
|
||||||
// Check the returned fix is for what we asked for.
|
// Check the returned fix is for what we asked for.
|
||||||
check(fix.of == fixOf)
|
check(fix.of == fixOf)
|
||||||
|
@ -2,13 +2,12 @@ package com.r3corda.protocols
|
|||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.r3corda.core.checkedAdd
|
import com.r3corda.core.checkedAdd
|
||||||
import com.r3corda.core.contracts.LedgerTransaction
|
|
||||||
import com.r3corda.core.contracts.SignedTransaction
|
|
||||||
import com.r3corda.core.contracts.WireTransaction
|
|
||||||
import com.r3corda.core.contracts.toLedgerTransaction
|
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.crypto.SecureHash
|
import com.r3corda.core.crypto.SecureHash
|
||||||
import com.r3corda.core.protocols.ProtocolLogic
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
|
import com.r3corda.core.transactions.LedgerTransaction
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
// TODO: This code is currently unit tested by TwoPartyTradeProtocolTests, it should have its own tests.
|
// TODO: This code is currently unit tested by TwoPartyTradeProtocolTests, it should have its own tests.
|
||||||
@ -33,6 +32,39 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
|
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
|
||||||
|
|
||||||
|
private fun topologicalSort(transactions: Collection<SignedTransaction>): List<SignedTransaction> {
|
||||||
|
// Construct txhash -> dependent-txs map
|
||||||
|
val forwardGraph = HashMap<SecureHash, HashSet<SignedTransaction>>()
|
||||||
|
transactions.forEach { tx ->
|
||||||
|
tx.tx.inputs.forEach { input ->
|
||||||
|
// Note that we use a LinkedHashSet here to make the traversal deterministic (as long as the input list is)
|
||||||
|
forwardGraph.getOrPut(input.txhash) { LinkedHashSet() }.add(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val visited = HashSet<SecureHash>(transactions.size)
|
||||||
|
val result = ArrayList<SignedTransaction>(transactions.size)
|
||||||
|
|
||||||
|
fun visit(transaction: SignedTransaction) {
|
||||||
|
if (transaction.id !in visited) {
|
||||||
|
visited.add(transaction.id)
|
||||||
|
forwardGraph[transaction.id]?.forEach {
|
||||||
|
visit(it)
|
||||||
|
}
|
||||||
|
result.add(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions.forEach {
|
||||||
|
visit(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.reverse()
|
||||||
|
require(result.size == transactions.size)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExcessivelyLargeTransactionGraph() : Exception()
|
class ExcessivelyLargeTransactionGraph() : Exception()
|
||||||
@ -61,7 +93,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call(): List<LedgerTransaction> {
|
override fun call(): List<LedgerTransaction> {
|
||||||
val newTxns: Iterable<SignedTransaction> = downloadDependencies(txHashes)
|
val newTxns: Iterable<SignedTransaction> = topologicalSort(downloadDependencies(txHashes))
|
||||||
|
|
||||||
// For each transaction, verify it and insert it into the database. As we are iterating over them in a
|
// For each transaction, verify it and insert it into the database. As we are iterating over them in a
|
||||||
// depth-first order, we should not encounter any verification failures due to missing data. If we fail
|
// depth-first order, we should not encounter any verification failures due to missing data. If we fail
|
||||||
@ -83,7 +115,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
|||||||
// be a clearer API if we do that. But for consistency with the other c'tor we currently do not.
|
// be a clearer API if we do that. But for consistency with the other c'tor we currently do not.
|
||||||
//
|
//
|
||||||
// If 'stx' is set, then 'wtx' is the contents (from the c'tor).
|
// If 'stx' is set, then 'wtx' is the contents (from the c'tor).
|
||||||
stx?.verifySignatures()
|
val wtx = stx?.verifySignatures() ?: wtx
|
||||||
wtx?.let {
|
wtx?.let {
|
||||||
fetchMissingAttachments(listOf(it))
|
fetchMissingAttachments(listOf(it))
|
||||||
val ltx = it.toLedgerTransaction(serviceHub)
|
val ltx = it.toLedgerTransaction(serviceHub)
|
||||||
@ -97,7 +129,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
|||||||
override val topic: String get() = throw UnsupportedOperationException()
|
override val topic: String get() = throw UnsupportedOperationException()
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun downloadDependencies(depsToCheck: Set<SecureHash>): List<SignedTransaction> {
|
private fun downloadDependencies(depsToCheck: Set<SecureHash>): Collection<SignedTransaction> {
|
||||||
// Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth
|
// Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth
|
||||||
// first traversal across the dependency graph.
|
// first traversal across the dependency graph.
|
||||||
//
|
//
|
||||||
@ -147,7 +179,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
|||||||
throw ExcessivelyLargeTransactionGraph()
|
throw ExcessivelyLargeTransactionGraph()
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultQ.values.reversed()
|
return resultQ.values
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -11,13 +11,15 @@ import com.r3corda.core.node.services.DEFAULT_SESSION_ID
|
|||||||
import com.r3corda.core.protocols.ProtocolLogic
|
import com.r3corda.core.protocols.ProtocolLogic
|
||||||
import com.r3corda.core.random63BitValue
|
import com.r3corda.core.random63BitValue
|
||||||
import com.r3corda.core.seconds
|
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.ProgressTracker
|
||||||
import com.r3corda.core.utilities.UntrustworthyData
|
import com.r3corda.core.utilities.UntrustworthyData
|
||||||
import com.r3corda.core.utilities.trace
|
import com.r3corda.core.utilities.trace
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import java.security.SignatureException
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,15 +100,11 @@ object TwoPartyDealProtocol {
|
|||||||
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
|
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
|
|
||||||
untrustedPartialTX.validate { stx ->
|
untrustedPartialTX.unwrap { stx ->
|
||||||
progressTracker.nextStep()
|
progressTracker.nextStep()
|
||||||
|
|
||||||
// Check that the tx proposed by the buyer is valid.
|
// Check that the tx proposed by the buyer is valid.
|
||||||
val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false)
|
val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
|
||||||
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
|
|
||||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
|
||||||
|
|
||||||
val wtx: WireTransaction = stx.tx
|
|
||||||
logger.trace { "Received partially signed transaction: ${stx.id}" }
|
logger.trace { "Received partially signed transaction: ${stx.id}" }
|
||||||
|
|
||||||
checkDependencies(stx)
|
checkDependencies(stx)
|
||||||
@ -242,7 +240,7 @@ object TwoPartyDealProtocol {
|
|||||||
val handshake = receive<Handshake<U>>(sessionID)
|
val handshake = receive<Handshake<U>>(sessionID)
|
||||||
|
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
handshake.validate {
|
handshake.unwrap {
|
||||||
return validateHandshake(it)
|
return validateHandshake(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,7 +252,7 @@ object TwoPartyDealProtocol {
|
|||||||
|
|
||||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||||
|
|
||||||
return sendAndReceive<SignaturesFromPrimary>(otherSide, theirSessionID, sessionID, stx).validate { it }
|
return sendAndReceive<SignaturesFromPrimary>(otherSide, theirSessionID, sessionID, stx).unwrap { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package com.r3corda.protocols
|
package com.r3corda.protocols
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import com.r3corda.core.contracts.SignedTransaction
|
|
||||||
import com.r3corda.core.contracts.TransactionVerificationException
|
import com.r3corda.core.contracts.TransactionVerificationException
|
||||||
import com.r3corda.core.contracts.WireTransaction
|
|
||||||
import com.r3corda.core.contracts.toLedgerTransaction
|
|
||||||
import com.r3corda.core.crypto.Party
|
import com.r3corda.core.crypto.Party
|
||||||
import com.r3corda.core.node.services.TimestampChecker
|
import com.r3corda.core.node.services.TimestampChecker
|
||||||
import com.r3corda.core.node.services.UniquenessProvider
|
import com.r3corda.core.node.services.UniquenessProvider
|
||||||
|
import com.r3corda.core.transactions.SignedTransaction
|
||||||
|
import com.r3corda.core.transactions.WireTransaction
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,10 +37,11 @@ class ValidatingNotaryProtocol(otherSide: Party,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkSignatures(stx: SignedTransaction) {
|
private fun checkSignatures(stx: SignedTransaction) {
|
||||||
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
|
try {
|
||||||
val missing = stx.verifySignatures(throwIfSignaturesAreMissing = false) - myKey
|
stx.verifySignatures(serviceHub.storageService.myLegalIdentity.owningKey)
|
||||||
|
} catch(e: SignedTransaction.SignaturesMissingException) {
|
||||||
if (missing.isNotEmpty()) throw NotaryException(NotaryError.SignaturesMissing(missing.toList()))
|
throw NotaryException(NotaryError.SignaturesMissing(e.missing))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user