mirror of
https://github.com/corda/corda.git
synced 2024-12-30 09:48:59 +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
|
||||
/experimental/build
|
||||
/docs/build/doctrees
|
||||
/test-utils/build
|
||||
/client/build
|
||||
|
||||
# gradle's 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_main.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_main.iml" group="buildSrc" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" filepath="$PROJECT_DIR$/.idea/modules/buildSrc_test.iml" group="buildSrc" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_integrationTest.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_main.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_main.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/client/client_test.iml" filepath="$PROJECT_DIR$/.idea/modules/client/client_test.iml" group="client" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts.iml" group="contracts" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_main.iml" group="contracts" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/contracts_test.iml" group="contracts" />
|
||||
@ -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_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_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_integrationTest.iml" group="node" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_main.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_main.iml" group="node" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/node/node_test.iml" filepath="$PROJECT_DIR$/.idea/modules/node/node_test.iml" group="node" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_integrationTest.iml" group="r3prototyping" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_main.iml" group="r3prototyping" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" filepath="$PROJECT_DIR$/.idea/modules/r3prototyping_test.iml" group="r3prototyping" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils.iml" group="test-utils" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_main.iml" group="test-utils" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" filepath="$PROJECT_DIR$/.idea/modules/test-utils/test-utils_test.iml" group="test-utils" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -42,7 +42,7 @@ Install the Oracle JDK 8u45 or higher. It is possible that OpenJDK will also wor
|
||||
|
||||
## 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/
|
||||
|
||||
|
41
build.gradle
41
build.gradle
@ -1,6 +1,6 @@
|
||||
buildscript {
|
||||
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.artemis_version = '1.3.0'
|
||||
ext.jackson_version = '2.8.0.rc2'
|
||||
@ -48,7 +48,7 @@ allprojects {
|
||||
|
||||
// Our version: bump this on release.
|
||||
group 'com.r3corda'
|
||||
version '0.3-SNAPSHOT'
|
||||
version '0.4-SNAPSHOT'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@ -102,21 +102,24 @@ mainClassName = 'com.r3corda.demos.TraderDemoKt'
|
||||
|
||||
dependencies {
|
||||
compile project(':node')
|
||||
// TODO: Demos should not depend on test code, but only use production APIs
|
||||
compile project(':test-utils')
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
compile "org.jetbrains.kotlinx:kotlinx-support-jdk8:0.2"
|
||||
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
||||
compile 'co.paralleluniverse:capsule:1.0.3'
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.assertj:assertj-core:3.4.1'
|
||||
testCompile 'com.pholser:junit-quickcheck-core:0.6'
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile 'junit:junit:4.12'
|
||||
integrationTestCompile 'org.assertj:assertj-core:${assertj_version}'
|
||||
integrationTestCompile project(':test-utils')
|
||||
}
|
||||
|
||||
// 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
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
test.finalizedBy(integrationTest)
|
||||
|
||||
|
||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
@ -196,46 +198,49 @@ applicationDistribution.into("bin") {
|
||||
fileMode = 0755
|
||||
}
|
||||
|
||||
task createCapsule(type: FatCapsule, dependsOn: 'quasarScan') {
|
||||
task buildCordaJAR(type: FatCapsule, dependsOn: 'quasarScan') {
|
||||
applicationClass 'com.r3corda.node.MainKt'
|
||||
archiveName 'corda.jar'
|
||||
applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class')
|
||||
|
||||
capsuleManifest {
|
||||
appClassPath = ["jolokia-agent-war-${project.ext.jolokia_version}.war"]
|
||||
systemProperties['log4j.configuration'] = 'log4j2.xml'
|
||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
|
||||
minJavaVersion = '1.8.0'
|
||||
caplets = ['CordaCaplet']
|
||||
}
|
||||
}
|
||||
|
||||
task createStandalone(dependsOn: 'createCapsule') << {
|
||||
task installTemplateNodes(dependsOn: 'buildCordaJAR') << {
|
||||
copy {
|
||||
from createCapsule.outputs.getFiles()
|
||||
from buildCordaJAR.outputs.getFiles()
|
||||
from 'config/dev/nameservernode.conf'
|
||||
into "${buildDir}/standalone/nameserver"
|
||||
into "${buildDir}/nodes/nameserver"
|
||||
rename 'nameservernode.conf', 'node.conf'
|
||||
}
|
||||
|
||||
copy {
|
||||
from createCapsule.outputs.getFiles()
|
||||
from buildCordaJAR.outputs.getFiles()
|
||||
from 'config/dev/generalnodea.conf'
|
||||
into "${buildDir}/standalone/nodea"
|
||||
into "${buildDir}/nodes/nodea"
|
||||
rename 'generalnodea.conf', 'node.conf'
|
||||
}
|
||||
|
||||
copy {
|
||||
from createCapsule.outputs.getFiles()
|
||||
from buildCordaJAR.outputs.getFiles()
|
||||
from 'config/dev/generalnodeb.conf'
|
||||
into "${buildDir}/standalone/nodeb"
|
||||
into "${buildDir}/nodes/nodeb"
|
||||
rename 'generalnodeb.conf', 'node.conf'
|
||||
}
|
||||
|
||||
delete("${buildDir}/standalone/runstandalone")
|
||||
def jarName = createCapsule.outputs.getFiles().getSingleFile().getName()
|
||||
delete("${buildDir}/nodes/runnodes")
|
||||
def jarName = buildCordaJAR.outputs.getFiles().getSingleFile().getName()
|
||||
copy {
|
||||
from "buildSrc/scripts/runstandalone"
|
||||
from "buildSrc/scripts/runnodes"
|
||||
filter { String line -> line.replace("JAR_NAME", jarName) }
|
||||
filter(org.apache.tools.ant.filters.FixCrLfFilter.class, eol: org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf"))
|
||||
into "${buildDir}/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
|
||||
# Creates three nodes. A network map and notary node and two regular nodes that can be extended with cordapps.
|
||||
|
||||
set -euo pipefail
|
||||
trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT
|
||||
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",
|
||||
myLegalName : "Bank A",
|
||||
nearestCity : "London",
|
||||
keyStorePassword : "cordacadevpass",
|
||||
trustStorePassword : "trustpass",
|
||||
artemisAddress : "localhost:31337",
|
||||
webAddress : "localhost:31339",
|
||||
hostNotaryServiceLocally: false,
|
||||
extraAdvertisedServiceIds: "corda.interest_rates",
|
||||
mapService : {
|
||||
hostServiceLocally : false,
|
||||
address : "localhost:12345",
|
||||
identity : "Notary Service"
|
||||
}
|
||||
basedir : "./nodea"
|
||||
myLegalName : "Bank A"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:31337"
|
||||
webAddress : "localhost:31339"
|
||||
hostNotaryServiceLocally: false
|
||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||
networkMapAddress : "localhost:12345"
|
||||
useHTTPS : false
|
||||
|
@ -1,14 +1,11 @@
|
||||
basedir : "./nodeb",
|
||||
myLegalName : "Bank B",
|
||||
nearestCity : "London",
|
||||
keyStorePassword : "cordacadevpass",
|
||||
trustStorePassword : "trustpass",
|
||||
artemisAddress : "localhost:31338",
|
||||
webAddress : "localhost:31340",
|
||||
hostNotaryServiceLocally: false,
|
||||
extraAdvertisedServiceIds: "corda.interest_rates",
|
||||
mapService : {
|
||||
hostServiceLocally : false,
|
||||
address : "localhost:12345",
|
||||
identity : "Notary Service"
|
||||
}
|
||||
basedir : "./nodeb"
|
||||
myLegalName : "Bank B"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:31338"
|
||||
webAddress : "localhost:31340"
|
||||
hostNotaryServiceLocally: false
|
||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||
networkMapAddress : "localhost:12345"
|
||||
useHTTPS : false
|
||||
|
@ -1,14 +1,10 @@
|
||||
basedir : "./nameserver",
|
||||
myLegalName : "Notary Service",
|
||||
nearestCity : "London",
|
||||
keyStorePassword : "cordacadevpass",
|
||||
trustStorePassword : "trustpass",
|
||||
artemisAddress : "localhost:12345",
|
||||
webAddress : "localhost:12346",
|
||||
hostNotaryServiceLocally: true,
|
||||
extraAdvertisedServiceIds: "",
|
||||
mapService : {
|
||||
hostServiceLocally : true,
|
||||
address : ${artemisAddress},
|
||||
identity : ${myLegalName}
|
||||
}
|
||||
basedir : "./nameserver"
|
||||
myLegalName : "Notary Service"
|
||||
nearestCity : "London"
|
||||
keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:12345"
|
||||
webAddress : "localhost:12346"
|
||||
hostNotaryServiceLocally: true
|
||||
extraAdvertisedServiceIds: ""
|
||||
useHTTPS : false
|
@ -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: CanonicalizerPlugin
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'http://oss.sonatype.org/content/repositories/snapshots'
|
||||
}
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
|
||||
testCompile project(':test-utils')
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
@ -87,4 +26,4 @@ sourceSets {
|
||||
srcDir "../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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: CanonicalizerPlugin
|
||||
|
||||
repositories {
|
||||
@ -85,4 +21,4 @@ sourceSets {
|
||||
srcDir "../../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ package com.r3corda.contracts.isolated
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
|
@ -2,7 +2,7 @@ package com.r3corda.core.node
|
||||
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.PartyAndReference
|
||||
import com.r3corda.core.contracts.TransactionBuilder
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.crypto.Party
|
||||
|
||||
interface DummyContractBackdoor {
|
||||
|
@ -3,19 +3,18 @@ package com.r3corda.contracts;
|
||||
import com.google.common.collect.*;
|
||||
import com.r3corda.contracts.asset.*;
|
||||
import com.r3corda.core.contracts.*;
|
||||
import static com.r3corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
import com.r3corda.core.contracts.Timestamp;
|
||||
import com.r3corda.core.contracts.TransactionForContract.*;
|
||||
import com.r3corda.core.contracts.clauses.*;
|
||||
import com.r3corda.core.crypto.*;
|
||||
import kotlin.Unit;
|
||||
import com.r3corda.core.transactions.*;
|
||||
import kotlin.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.security.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.*;
|
||||
|
||||
import static com.r3corda.core.contracts.ContractsDSL.*;
|
||||
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.
|
||||
*/
|
||||
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();
|
||||
|
||||
public static class State implements ContractState, ICommercialPaperState {
|
||||
public static class State implements OwnableState, ICommercialPaperState {
|
||||
private PartyAndReference issuance;
|
||||
private PublicKey owner;
|
||||
private Amount<Issued<Currency>> faceValue;
|
||||
@ -54,6 +52,12 @@ public class JavaCommercialPaper implements Contract {
|
||||
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) {
|
||||
return new State(newIssuance, this.owner, this.faceValue, this.maturityDate);
|
||||
}
|
||||
@ -70,6 +74,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
return issuance;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PublicKey getOwner() {
|
||||
return owner;
|
||||
}
|
||||
@ -86,7 +91,6 @@ public class JavaCommercialPaper implements Contract {
|
||||
@Override
|
||||
public Contract getContract() {
|
||||
return JCP_PROGRAM_ID;
|
||||
//return SecureHash.sha256("java commercial paper (this should be a bytecode hash)");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -128,44 +132,17 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
public interface Clause {
|
||||
abstract class AbstractGroup implements GroupClause<State, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public MatchBehaviour getIfNotMatched() {
|
||||
return MatchBehaviour.CONTINUE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
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;
|
||||
public interface Clauses {
|
||||
class Group extends GroupClauseVerifier<State, Commands, State> {
|
||||
// This complains because we're passing generic types into a varargs, but it is valid so we suppress the
|
||||
// warning.
|
||||
@SuppressWarnings("unchecked")
|
||||
Group() {
|
||||
super(new AnyComposition<>(
|
||||
new Clauses.Redeem(),
|
||||
new Clauses.Move(),
|
||||
new Clauses.Issue()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -175,7 +152,7 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Move extends AbstractGroup {
|
||||
class Move extends Clause<State, Commands, State> {
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
@ -184,11 +161,11 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
@NotNull State groupingKey) {
|
||||
AuthenticatedObject<Commands.Move> cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class);
|
||||
// There should be only a single input due to aggregation above
|
||||
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
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
@ -215,11 +192,11 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
@NotNull State groupingKey) {
|
||||
AuthenticatedObject<Commands.Redeem> cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class);
|
||||
|
||||
// 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
|
||||
@Override
|
||||
public Set<Class<? extends CommandData>> getRequiredCommands() {
|
||||
@ -257,14 +234,13 @@ public class JavaCommercialPaper implements Contract {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<CommandData> verify(@NotNull TransactionForContract tx,
|
||||
public Set<Commands> verify(@NotNull TransactionForContract tx,
|
||||
@NotNull List<? extends State> inputs,
|
||||
@NotNull List<? extends State> outputs,
|
||||
@NotNull Collection<? extends AuthenticatedObject<? extends CommandData>> commands,
|
||||
@NotNull State token) {
|
||||
@NotNull List<? extends AuthenticatedObject<? extends Commands>> commands,
|
||||
@NotNull State groupingKey) {
|
||||
AuthenticatedObject<Commands.Issue> cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class);
|
||||
State output = single(outputs);
|
||||
Party notary = cmd.getValue().notary;
|
||||
Timestamp timestampCommand = tx.getTimestamp();
|
||||
Instant time = null == timestampCommand
|
||||
? null
|
||||
@ -291,49 +267,28 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
|
||||
class Redeem implements Commands {
|
||||
private final Party notary;
|
||||
|
||||
public Redeem(Party setNotary) {
|
||||
this.notary = setNotary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) { return obj instanceof Redeem; }
|
||||
}
|
||||
|
||||
class Issue implements Commands {
|
||||
private final Party notary;
|
||||
|
||||
public Issue(Party setNotary) {
|
||||
this.notary = setNotary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Issue) {
|
||||
Issue other = (Issue)obj;
|
||||
return notary.equals(other.notary);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() { return notary.hashCode(); }
|
||||
public boolean equals(Object obj) { return obj instanceof Issue; }
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Collection<AuthenticatedObject<CommandData>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
private List<AuthenticatedObject<Commands>> extractCommands(@NotNull TransactionForContract tx) {
|
||||
return tx.getCommands()
|
||||
.stream()
|
||||
.filter((AuthenticatedObject<CommandData> command) -> command.getValue() instanceof Commands)
|
||||
.map((AuthenticatedObject<CommandData> command) -> new AuthenticatedObject<>(command.getSigners(), command.getSigningParties(), (Commands) command.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
@ -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) {
|
||||
State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate);
|
||||
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 {
|
||||
new Cash().generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), wallet, null);
|
||||
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) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.contracts.asset.FungibleAsset
|
||||
import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||
import com.r3corda.contracts.asset.sumCashBy
|
||||
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.toStringShort
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
@ -49,10 +51,7 @@ class CommercialPaper : Contract {
|
||||
val maturityDate: Instant
|
||||
)
|
||||
|
||||
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clauses.Group()), extractCommands(tx))
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx, Clauses.Group(), tx.commands.select<Commands>())
|
||||
|
||||
data class State(
|
||||
val issuance: PartyAndReference,
|
||||
@ -79,25 +78,16 @@ class CommercialPaper : Contract {
|
||||
}
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Issued<Terms>>() {
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val clauses = listOf(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue()
|
||||
)
|
||||
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Terms>>(
|
||||
AnyComposition(
|
||||
Redeem(),
|
||||
Move(),
|
||||
Issue())) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Terms>>>
|
||||
= tx.groupStates<State, Issued<Terms>> { it.token }
|
||||
}
|
||||
|
||||
abstract class AbstractGroupClause: GroupClause<State, Issued<Terms>> {
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Terms>(
|
||||
class Issue : AbstractIssue<State, Commands, Terms>(
|
||||
{ map { Amount(it.faceValue.quantity, it.token) }.sumOrThrow() },
|
||||
{ token -> map { Amount(it.faceValue.quantity, it.token) }.sumOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
@ -105,10 +95,10 @@ class CommercialPaper : Contract {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
val consumedCommands = super.verify(tx, inputs, outputs, commands, token)
|
||||
val command = commands.requireSingleCommand<Commands.Issue>()
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey)
|
||||
commands.requireSingleCommand<Commands.Issue>()
|
||||
val timestamp = tx.timestamp
|
||||
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 fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
val command = commands.requireSingleCommand<Commands.Move>()
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
@ -138,15 +128,14 @@ class CommercialPaper : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Redeem(): AbstractGroupClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Commands.Redeem::class.java)
|
||||
class Redeem(): Clause<State, Commands, Issued<Terms>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Redeem::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms>?): Set<Commands> {
|
||||
// TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date)
|
||||
// before requiring a single command
|
||||
val command = commands.requireSingleCommand<Commands.Redeem>()
|
||||
@ -169,9 +158,9 @@ class CommercialPaper : Contract {
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
data class Redeem(val notary: Party) : Commands
|
||||
data class Issue(val notary: Party, override val nonce: Long = random63BitValue()) : IssueCommand, Commands
|
||||
data class Move(override val contractHash: SecureHash? = null) : FungibleAsset.Commands.Move, Commands
|
||||
class Redeem : TypeOnlyCommandData(), 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 {
|
||||
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) }
|
||||
Cash().generateSpend(tx, amount, paper.state.data.owner, wallet)
|
||||
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
|
||||
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.contracts.asset.InsufficientBalanceException
|
||||
import com.r3corda.contracts.asset.sumCashBy
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.NullPublicKey
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
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 java.security.PublicKey
|
||||
import java.time.Instant
|
||||
@ -30,8 +34,7 @@ class CommercialPaperLegacy : Contract {
|
||||
val maturityDate: Instant
|
||||
) : OwnableState, ICommercialPaperState {
|
||||
override val contract = CP_LEGACY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
get() = listOf(owner)
|
||||
override val participants = listOf(owner)
|
||||
|
||||
fun withoutOwner() = copy(owner = NullPublicKey)
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
@ -46,11 +49,11 @@ class CommercialPaperLegacy : Contract {
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Move: TypeOnlyCommandData(), Commands
|
||||
data class Redeem(val notary: Party) : Commands
|
||||
class Move : TypeOnlyCommandData(), 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.
|
||||
// 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) {
|
||||
@ -74,8 +77,8 @@ class CommercialPaperLegacy : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
is Commands.Redeem -> {
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
val input = inputs.single()
|
||||
val received = tx.outputs.sumCashBy(input.owner)
|
||||
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)
|
||||
"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.
|
||||
// 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.
|
||||
"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.SecureHash
|
||||
import com.r3corda.core.protocols.ProtocolLogicRefFactory
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow
|
||||
import com.r3corda.protocols.TwoPartyDealProtocol
|
||||
import org.apache.commons.jexl3.JexlBuilder
|
||||
@ -447,20 +448,14 @@ class InterestRateSwap() : Contract {
|
||||
fixingCalendar, index, indexSource, indexTenor)
|
||||
}
|
||||
|
||||
fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
|
||||
= tx.commands.select<Commands>()
|
||||
override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select<Commands>())
|
||||
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, listOf(Clause.Timestamped(), Clause.Group()), extractCommands(tx))
|
||||
|
||||
interface Clause {
|
||||
interface Clauses {
|
||||
/**
|
||||
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
|
||||
* helper functions for the clauses.
|
||||
*/
|
||||
abstract class AbstractIRSClause : GroupClause<State, String> {
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.CONTINUE
|
||||
|
||||
abstract class AbstractIRSClause : Clause<State, Commands, UniqueIdentifier>() {
|
||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||
fun checkLegDates(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
@ -502,23 +497,18 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<State, String>() {
|
||||
override val ifMatched = MatchBehaviour.END
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, String>>
|
||||
class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyComposition(Agree(), Fix(), Pay(), Mature())) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, UniqueIdentifier>>
|
||||
// Group by Trade ID for in / out states
|
||||
= tx.groupStates() { state -> state.common.tradeID }
|
||||
|
||||
override val clauses = listOf(Agree(), Fix(), Pay(), Mature())
|
||||
= tx.groupStates() { state -> state.linearId }
|
||||
}
|
||||
|
||||
class Timestamped : SingleClause {
|
||||
override val ifMatched = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched = MatchBehaviour.ERROR
|
||||
override val requiredCommands = emptySet<Class<out CommandData>>()
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
class Timestamped : Clause<ContractState, Commands, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Unit?): Set<Commands> {
|
||||
require(tx.timestamp?.midpoint != null) { "must be timestamped" }
|
||||
// We return an empty set because we don't process any commands
|
||||
return emptySet()
|
||||
@ -526,13 +516,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
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,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
@ -562,13 +552,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
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,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Refix>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
val prevIrs = inputs.filterIsInstance<State>().single()
|
||||
@ -607,13 +597,13 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
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,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Pay>()
|
||||
requireThat {
|
||||
"Payments not supported / verifiable yet" by false
|
||||
@ -623,17 +613,18 @@ class InterestRateSwap() : Contract {
|
||||
}
|
||||
|
||||
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,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: String): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Mature>()
|
||||
val irs = inputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
"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)
|
||||
@ -656,11 +647,12 @@ class InterestRateSwap() : Contract {
|
||||
val fixedLeg: FixedLeg,
|
||||
val floatingLeg: FloatingLeg,
|
||||
val calculation: Calculation,
|
||||
val common: Common
|
||||
val common: Common,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID)
|
||||
) : FixableDealState, SchedulableState {
|
||||
|
||||
override val contract = IRS_PROGRAM_ID
|
||||
override val thread = SecureHash.sha256(common.tradeID)
|
||||
|
||||
override val ref = common.tradeID
|
||||
|
||||
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.NoZeroSizedOutputs
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClauseVerifier
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import java.math.BigInteger
|
||||
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
|
||||
* 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:
|
||||
* 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.
|
||||
*/
|
||||
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 clauses = listOf(Clauses.Group())
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<Cash.Commands>()
|
||||
override val conserveClause: AbstractConserveAmount<State, Commands, Currency> = Clauses.ConserveAmount()
|
||||
override fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): List<AuthenticatedObject<Cash.Commands>>
|
||||
= commands.select<Cash.Commands>()
|
||||
|
||||
interface Clauses {
|
||||
class Group : GroupClauseVerifier<State, Issued<Currency>>() {
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State, Currency>(),
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Currency>>(AllComposition<State, Commands, Issued<Currency>>(
|
||||
NoZeroSizedOutputs<State, Commands, Currency>(),
|
||||
FirstComposition<State, Commands, Issued<Currency>>(
|
||||
Issue(),
|
||||
ConserveAmount())
|
||||
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<State, Issued<Currency>>>
|
||||
= tx.groupStates<State, Issued<Currency>> { it.issuanceDef }
|
||||
}
|
||||
|
||||
class Issue : AbstractIssue<State, Currency>(
|
||||
class Issue : AbstractIssue<State, Commands, Currency>(
|
||||
sum = { sumCash() },
|
||||
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. */
|
||||
@ -86,7 +84,7 @@ class Cash : OnLedgerAsset<Currency, Cash.State>() {
|
||||
: this(Amount(amount.quantity, Issued(deposit, amount.token)), owner)
|
||||
|
||||
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 issuanceDef = amount.token
|
||||
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 generateIssueCommand() = Commands.Issue()
|
||||
override fun generateMoveCommand() = Commands.Move()
|
||||
|
||||
override fun verify(tx: TransactionForContract)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.commands))
|
||||
}
|
||||
|
||||
// Small DSL extensions.
|
||||
|
@ -5,11 +5,13 @@ import com.r3corda.contracts.clause.AbstractIssue
|
||||
import com.r3corda.contracts.clause.NoZeroSizedOutputs
|
||||
import com.r3corda.core.contracts.*
|
||||
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.SecureHash
|
||||
import com.r3corda.core.crypto.newSecureRandom
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
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
|
||||
* 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).
|
||||
*
|
||||
* 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.
|
||||
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.State>() {
|
||||
class CommodityContract : OnLedgerAsset<Commodity, CommodityContract.Commands, CommodityContract.State>() {
|
||||
/**
|
||||
* TODO:
|
||||
* 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 conserveClause: AbstractConserveAmount<State, Commodity> = Clauses.ConserveAmount()
|
||||
override val conserveClause: AbstractConserveAmount<State, Commands, Commodity> = Clauses.ConserveAmount()
|
||||
|
||||
/**
|
||||
* 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
|
||||
* each group.
|
||||
*/
|
||||
class Group : GroupClauseVerifier<State, Issued<Commodity>>() {
|
||||
/**
|
||||
* The group clause does not depend on any commands being present, so something has gone terribly wrong if
|
||||
* it doesn't match.
|
||||
*/
|
||||
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()
|
||||
)
|
||||
|
||||
class Group : GroupClauseVerifier<State, Commands, Issued<Commodity>>(AnyComposition(
|
||||
NoZeroSizedOutputs<State, Commands, Commodity>(),
|
||||
Issue(),
|
||||
ConserveAmount())) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class Issue : AbstractIssue<State, Commodity>(
|
||||
class Issue : AbstractIssue<State, Commands, Commodity>(
|
||||
sum = { sumCommodities() },
|
||||
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.
|
||||
*/
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commodity>()
|
||||
class ConserveAmount : AbstractConserveAmount<State, Commands, Commodity>()
|
||||
}
|
||||
|
||||
/** 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>
|
||||
}
|
||||
override val clauses = listOf(Clauses.Group())
|
||||
override fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<CommodityContract.Commands>()
|
||||
override fun verify(tx: TransactionForContract)
|
||||
= verifyClause(tx, Clauses.Group(), extractCommands(tx.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.
|
||||
|
@ -29,7 +29,10 @@ interface FungibleAsset<T> : OwnableState {
|
||||
val deposit: PartyAndReference
|
||||
val issuanceDef: 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>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: PublicKey
|
||||
|
@ -4,16 +4,11 @@ import com.google.common.annotations.VisibleForTesting
|
||||
import com.r3corda.contracts.clause.*
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.*
|
||||
import com.r3corda.core.crypto.NullPublicKey
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.testing.MINI_CORP
|
||||
import com.r3corda.core.testing.TEST_TX_TIME
|
||||
import com.r3corda.core.utilities.Emoji
|
||||
import com.r3corda.core.utilities.NonEmptySet
|
||||
import com.r3corda.core.utilities.nonEmptySetOf
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.utilities.*
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
@ -43,25 +38,27 @@ class Obligation<P> : 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")
|
||||
private val clauses = listOf(InterceptorClause(Clauses.VerifyLifecycle<P>(), Clauses.Net<P>()),
|
||||
Clauses.Group<P>())
|
||||
|
||||
interface Clauses {
|
||||
/**
|
||||
* Parent clause for clauses that operate on grouped states (those which are fungible).
|
||||
*/
|
||||
class Group<P> : GroupClauseVerifier<State<P>, Issued<Terms<P>>>() {
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.ERROR
|
||||
override val clauses = listOf(
|
||||
NoZeroSizedOutputs<State<P>, Terms<P>>(),
|
||||
SetLifecycle<P>(),
|
||||
VerifyLifecycle<P>(),
|
||||
Settle<P>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
|
||||
class Group<P> : GroupClauseVerifier<State<P>, Commands, Issued<Terms<P>>>(
|
||||
AllComposition(
|
||||
NoZeroSizedOutputs<State<P>, Commands, Terms<P>>(),
|
||||
FirstComposition(
|
||||
SetLifecycle<P>(),
|
||||
AllComposition(
|
||||
VerifyLifecycle<State<P>, Commands, Issued<Terms<P>>, P>(),
|
||||
FirstComposition(
|
||||
Settle<P>(),
|
||||
Issue(),
|
||||
ConserveAmount()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
override fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<Obligation.State<P>, Issued<Terms<P>>>>
|
||||
= tx.groupStates<Obligation.State<P>, Issued<Terms<P>>> { it.issuanceDef }
|
||||
}
|
||||
@ -69,58 +66,64 @@ class Obligation<P> : Contract {
|
||||
/**
|
||||
* Generic issuance clause
|
||||
*/
|
||||
class Issue<P> : AbstractIssue<State<P>, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
||||
override val requiredCommands = setOf(Obligation.Commands.Issue::class.java)
|
||||
class Issue<P> : AbstractIssue<State<P>, Commands, Terms<P>>({ -> sumObligations() }, { token: Issued<Terms<P>> -> sumObligationsOrZero(token) }) {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Issue::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
class SetLifecycle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override val requiredCommands = setOf(Commands.SetLifecycle::class.java)
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
class SetLifecycle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.SetLifecycle::class.java)
|
||||
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||
val command = commands.requireSingleCommand<Commands.SetLifecycle>()
|
||||
Obligation<P>().verifySetLifecycleCommand(inputs, outputs, tx, command)
|
||||
return setOf(command.value)
|
||||
}
|
||||
|
||||
override fun toString(): String = "Set obligation lifecycle"
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligation-specific clause for settling an outstanding obligation by witnessing
|
||||
* change of ownership of other states to fulfil
|
||||
*/
|
||||
class Settle<P> : GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
override val requiredCommands = setOf(Commands.Settle::class.java)
|
||||
override val ifMatched: MatchBehaviour = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour = MatchBehaviour.CONTINUE
|
||||
|
||||
class Settle<P> : Clause<State<P>, Commands, Issued<Terms<P>>>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Settle::class.java)
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Issued<Terms<P>>?): Set<Commands> {
|
||||
require(groupingKey != null)
|
||||
val command = commands.requireSingleCommand<Commands.Settle<P>>()
|
||||
val obligor = token.issuer.party
|
||||
val template = token.product
|
||||
val obligor = groupingKey!!.issuer.party
|
||||
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 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
|
||||
|
||||
@ -166,7 +169,7 @@ class Obligation<P> : Contract {
|
||||
for ((beneficiary, obligations) in inputs.groupBy { it.owner }) {
|
||||
val settled = amountReceivedByOwner[beneficiary]?.sumFungibleOrNull<P>()
|
||||
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" }
|
||||
totalPenniesSettled += settled.quantity
|
||||
}
|
||||
@ -185,7 +188,7 @@ class Obligation<P> : Contract {
|
||||
"signatures are present from all obligors" by command.signers.containsAll(requiredSigners)
|
||||
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
|
||||
"at obligor ${obligor.name} the obligations after settlement balance" by
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, token))
|
||||
(inputAmount == outputAmount + Amount(totalPenniesSettled, groupingKey))
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
@ -197,26 +200,15 @@ class Obligation<P> : Contract {
|
||||
* any lifecycle change clause, which is the only clause that involve
|
||||
* non-standard lifecycle states on input/output.
|
||||
*/
|
||||
class VerifyLifecycle<P> : SingleClause, GroupClause<State<P>, Issued<Terms<P>>> {
|
||||
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>>()
|
||||
)
|
||||
|
||||
class VerifyLifecycle<S: ContractState, C: CommandData, T: Any, P> : Clause<S, C, T>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<State<P>>,
|
||||
outputs: List<State<P>>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<Terms<P>>): Set<CommandData>
|
||||
= verify(inputs, outputs)
|
||||
|
||||
fun verify(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>): Set<CommandData> {
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: T?): Set<C>
|
||||
= verify(inputs.filterIsInstance<State<P>>(), outputs.filterIsInstance<State<P>>())
|
||||
private fun verify(inputs: List<State<P>>,
|
||||
outputs: List<State<P>>): Set<C> {
|
||||
requireThat {
|
||||
"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 }
|
||||
@ -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
|
||||
* 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.
|
||||
@ -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>>
|
||||
}
|
||||
|
||||
private fun extractCommands(tx: TransactionForContract): List<AuthenticatedObject<FungibleAsset.Commands>>
|
||||
= tx.commands.select<Obligation.Commands>()
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
|
||||
override fun verify(tx: TransactionForContract) = verifyClause<Commands>(tx, FirstComposition<ContractState, Commands, Unit>(
|
||||
Clauses.Net<Commands, P>(),
|
||||
Clauses.Group<P>()
|
||||
), tx.commands.select<Obligation.Commands>())
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* 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.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<Terms<P>>>,
|
||||
changeKey: PublicKey, assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
|
||||
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, changeKey, assetStates,
|
||||
assetStates: List<StateAndRef<Obligation.State<P>>>): PublicKey
|
||||
= Clauses.ConserveAmount<P>().generateExit(tx, amountIssued, assetStates,
|
||||
deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) },
|
||||
generateMoveCommand = { -> Commands.Move() },
|
||||
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>.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>
|
||||
get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME)
|
||||
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.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.transactions.TransactionBuilder
|
||||
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
|
||||
* the issuer/depositRefs and just examine the amount fields.
|
||||
*/
|
||||
abstract class OnLedgerAsset<T : Any, S : FungibleAsset<T>> : Contract {
|
||||
abstract val clauses: List<SingleClause>
|
||||
abstract fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
|
||||
abstract val conserveClause: AbstractConserveAmount<S, T>
|
||||
|
||||
override fun verify(tx: TransactionForContract) = verifyClauses(tx, clauses, extractCommands(tx))
|
||||
abstract class OnLedgerAsset<T : Any, C: CommandData, S : FungibleAsset<T>> : Contract {
|
||||
abstract fun extractCommands(commands: Collection<AuthenticatedObject<CommandData>>): Collection<AuthenticatedObject<C>>
|
||||
abstract val conserveClause: AbstractConserveAmount<S, C, T>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun generateExit(tx: TransactionBuilder, amountIssued: Amount<Issued<T>>,
|
||||
changeKey: PublicKey, assetStates: List<StateAndRef<S>>): PublicKey
|
||||
= conserveClause.generateExit(tx, amountIssued, changeKey, assetStates,
|
||||
assetStates: List<StateAndRef<S>>): PublicKey
|
||||
= conserveClause.generateExit(tx, amountIssued, assetStates,
|
||||
deriveState = { state, amount, owner -> deriveState(state, amount, owner) },
|
||||
generateMoveCommand = { -> generateMoveCommand() },
|
||||
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.sumFungibleOrZero
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
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;
|
||||
* errors on no-match, ends on match.
|
||||
*/
|
||||
abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause<S, 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()
|
||||
|
||||
abstract class AbstractConserveAmount<S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
|
||||
/**
|
||||
* 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 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
|
||||
* 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.
|
||||
*/
|
||||
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>,
|
||||
generateMoveCommand: () -> CommandData,
|
||||
generateExitCommand: (Amount<Issued<T>>) -> CommandData): PublicKey {
|
||||
val owner = assetStates.map { it.state.data.owner }.toSet().single()
|
||||
val currency = amountIssued.token.product
|
||||
val amount = Amount(amountIssued.quantity, currency)
|
||||
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) {
|
||||
// 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()
|
||||
|
||||
for (state in gathered) tx.addInputState(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
|
||||
}
|
||||
|
||||
@ -178,26 +172,31 @@ abstract class AbstractConserveAmount<S: FungibleAsset<T>, T: Any> : GroupClause
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<T>): Set<CommandData> {
|
||||
val inputAmount: Amount<Issued<T>> = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for group $token")
|
||||
val deposit = token.issuer
|
||||
val outputAmount: Amount<Issued<T>> = outputs.sumFungibleOrZero(token)
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
require(groupingKey != null) { "Conserve amount clause can only be used on grouped states" }
|
||||
val matchedCommands = commands.filter { command -> command.value is FungibleAsset.Commands.Move || command.value is FungibleAsset.Commands.Exit<*> }
|
||||
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.
|
||||
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
|
||||
// If we want to remove assets from the ledger, that must be signed for by the issuer and owner.
|
||||
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 amountExitingLedger: Amount<Issued<T>> = exitCommand?.value?.amount ?: Amount(0, token)
|
||||
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, groupingKey)
|
||||
|
||||
requireThat {
|
||||
"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)
|
||||
}
|
||||
|
||||
return listOf(exitCommand?.value, verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, tx))
|
||||
.filter { it != null }
|
||||
.requireNoNulls().toSet()
|
||||
verifyMoveCommand<FungibleAsset.Commands.Move>(inputs, commands)
|
||||
|
||||
// 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
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 sumOrZero: List<S>.(token: Issued<T>) -> Amount<Issued<T>>
|
||||
) : GroupClause<S, Issued<T>> {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
|
||||
) : Clause<S, C, Issued<T>>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<T>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
require(groupingKey != null)
|
||||
// TODO: Take in matched commands as a parameter
|
||||
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).
|
||||
|
||||
// The grouping already ensures that all outputs have the same deposit reference and token.
|
||||
val issuer = token.issuer.party
|
||||
val inputAmount = inputs.sumOrZero(token)
|
||||
val issuer = groupingKey!!.issuer.party
|
||||
val inputAmount = inputs.sumOrZero(groupingKey)
|
||||
val outputAmount = outputs.sum()
|
||||
requireThat {
|
||||
"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)
|
||||
}
|
||||
|
||||
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.sumAmountsDue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.SingleClause
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -43,25 +42,25 @@ data class MultilateralNetState<P>(
|
||||
* Clause for netting contract states. Currently only supports obligation contract.
|
||||
*/
|
||||
// TODO: Make this usable for any nettable contract states
|
||||
open class NetClause<P> : SingleClause {
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.END
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = setOf(Obligation.Commands.Net::class.java)
|
||||
open class NetClause<C: CommandData, P> : Clause<ContractState, C, Unit>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Obligation.Commands.Net::class.java)
|
||||
|
||||
@Suppress("ConvertLambdaToReference")
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
val command = commands.requireSingleCommand<Obligation.Commands.Net>()
|
||||
override fun verify(tx: TransactionForContract,
|
||||
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) {
|
||||
NetType.CLOSE_OUT -> tx.groupStates { it: Obligation.State<P> -> it.bilateralNetState }
|
||||
NetType.PAYMENT -> tx.groupStates { it: Obligation.State<P> -> it.multilateralNetState }
|
||||
}
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
verifyNetCommand(inputs, outputs, command, key)
|
||||
for ((groupInputs, groupOutputs, key) in groups) {
|
||||
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
|
||||
fun verifyNetCommand(inputs: List<Obligation.State<P>>,
|
||||
outputs: List<Obligation.State<P>>,
|
||||
command: AuthenticatedObject<Obligation.Commands.Net>,
|
||||
command: AuthenticatedObject<NetCommand>,
|
||||
netState: NetState<P>) {
|
||||
val template = netState.template
|
||||
// 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.core.contracts.*
|
||||
import com.r3corda.core.contracts.clauses.GroupClause
|
||||
import com.r3corda.core.contracts.clauses.MatchBehaviour
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
|
||||
/**
|
||||
* Clause for fungible asset contracts, which enforces that no output state should have
|
||||
* a balance of zero.
|
||||
*/
|
||||
open class NoZeroSizedOutputs<in S: FungibleAsset<T>, T: Any> : GroupClause<S, Issued<T>> {
|
||||
override val ifMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.CONTINUE
|
||||
override val ifNotMatched: MatchBehaviour
|
||||
get() = MatchBehaviour.ERROR
|
||||
override val requiredCommands: Set<Class<CommandData>>
|
||||
get() = emptySet()
|
||||
|
||||
open class NoZeroSizedOutputs<in S : FungibleAsset<T>, C : CommandData, T : Any> : Clause<S, C, Issued<T>>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
outputs: List<S>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>,
|
||||
token: Issued<T>): Set<CommandData> {
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Issued<T>?): Set<C> {
|
||||
requireThat {
|
||||
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
|
||||
}
|
||||
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.core.contracts.Amount
|
||||
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.crypto.Party
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.testing.DUMMY_NOTARY
|
||||
import com.r3corda.core.testing.DUMMY_NOTARY_KEY
|
||||
import java.security.KeyPair
|
||||
import com.r3corda.core.utilities.DUMMY_NOTARY
|
||||
import java.security.PublicKey
|
||||
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 {
|
||||
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
|
||||
val amounts = LongArray(numStates)
|
||||
val baseSize = howMuch.quantity / numStates
|
||||
val numSlots = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
|
||||
val baseSize = howMuch.quantity / numSlots
|
||||
check(baseSize > 0) { baseSize }
|
||||
var filledSoFar = 0L
|
||||
for (i in 0..numStates - 1) {
|
||||
if (i < numStates - 1) {
|
||||
// Adjust the amount a bit up or down, to give more realistic amounts (not all identical).
|
||||
amounts[i] = baseSize + (baseSize / 2 * (rng.nextDouble() - 0.5)).toLong()
|
||||
filledSoFar += amounts[i]
|
||||
|
||||
val amounts = LongArray(numSlots) { baseSize }
|
||||
var distanceFromGoal = 0L
|
||||
// If we want 10 slots then max adjust is 0.1, so even if all random numbers come out to the largest downward
|
||||
// adjustment possible, the last slot ends at zero. With 20 slots, max adjust is 0.05 etc.
|
||||
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 {
|
||||
// Handle inexact rounding.
|
||||
amounts[i] = howMuch.quantity - filledSoFar
|
||||
amounts[i] += distanceFromGoal
|
||||
}
|
||||
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
|
||||
}
|
@ -7,16 +7,17 @@ import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.signWithECDSA
|
||||
import com.r3corda.core.crypto.toStringsShort
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import com.r3corda.core.utilities.ProgressTracker
|
||||
import com.r3corda.core.utilities.trace
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
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.
|
||||
val ourSignature = signWithOurKey(partialTX)
|
||||
val notarySignature = getNotarySignature(partialTX)
|
||||
|
||||
return sendSignatures(partialTX, ourSignature, notarySignature)
|
||||
}
|
||||
|
||||
@ -118,16 +118,11 @@ object TwoPartyTradeProtocol {
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
maybeSTX.validate {
|
||||
maybeSTX.unwrap {
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs: Set<PublicKey> = it.verifySignatures(throwIfSignaturesAreMissing = false)
|
||||
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
|
||||
val wtx: WireTransaction = it.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
|
||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||
|
||||
// Download and check all the things that this transaction depends on and verify it is contract-valid,
|
||||
@ -212,7 +207,7 @@ object TwoPartyTradeProtocol {
|
||||
val maybeTradeRequest = receive<SellerTradeInfo>(sessionID)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
maybeTradeRequest.validate {
|
||||
maybeTradeRequest.unwrap {
|
||||
// What is the seller trying to sell us?
|
||||
val asset = it.assetForSale.state.data
|
||||
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.
|
||||
|
||||
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 {
|
||||
|
@ -6,7 +6,8 @@ import kotlin.*;
|
||||
import org.junit.*;
|
||||
|
||||
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
|
||||
|
@ -6,9 +6,15 @@ import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.days
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
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.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
@ -17,6 +23,8 @@ import java.util.*
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
// TODO: The generate functions aren't tested by these tests: add them.
|
||||
|
||||
interface ICommercialPaperTestTemplate {
|
||||
fun getPaper(): ICommercialPaperState
|
||||
fun getIssueCommand(notary: Party): CommandData
|
||||
@ -32,8 +40,8 @@ class JavaCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
TEST_TX_TIME + 7.days
|
||||
)
|
||||
|
||||
override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue(notary)
|
||||
override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem(notary)
|
||||
override fun getIssueCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand(notary: Party): CommandData = JavaCommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = JavaCommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
@ -45,8 +53,8 @@ class KotlinCommercialPaperTest() : ICommercialPaperTestTemplate {
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
|
||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue(notary)
|
||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem(notary)
|
||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaper.Commands.Issue()
|
||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaper.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = CommercialPaper.Commands.Move()
|
||||
}
|
||||
|
||||
@ -58,8 +66,8 @@ class KotlinCommercialPaperLegacyTest() : ICommercialPaperTestTemplate {
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
|
||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue(notary)
|
||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem(notary)
|
||||
override fun getIssueCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Issue()
|
||||
override fun getRedeemCommand(notary: Party): CommandData = CommercialPaperLegacy.Commands.Redeem()
|
||||
override fun getMoveCommand(): CommandData = CommercialPaperLegacy.Commands.Move()
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,16 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
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 java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
@ -394,9 +401,10 @@ class IRSTests {
|
||||
|
||||
@Test
|
||||
fun `ensure failure occurs when there are inbound states for an agreement command`() {
|
||||
val irs = singleIRS()
|
||||
transaction {
|
||||
input() { singleIRS() }
|
||||
output("irs post agreement") { singleIRS() }
|
||||
input() { irs }
|
||||
output("irs post agreement") { irs }
|
||||
command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this `fails with` "There are no in states for an agreement"
|
||||
@ -665,10 +673,11 @@ class IRSTests {
|
||||
transaction("Agreement") {
|
||||
output("irs post agreement2") {
|
||||
irs.copy(
|
||||
irs.fixedLeg,
|
||||
irs.floatingLeg,
|
||||
irs.calculation,
|
||||
irs.common.copy(tradeID = "t2")
|
||||
linearId = UniqueIdentifier("t2"),
|
||||
fixedLeg = irs.fixedLeg,
|
||||
floatingLeg = irs.floatingLeg,
|
||||
calculation = irs.calculation,
|
||||
common = irs.common.copy(tradeID = "t2")
|
||||
)
|
||||
}
|
||||
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.SecureHash
|
||||
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 java.security.PublicKey
|
||||
import java.util.*
|
||||
@ -17,7 +21,9 @@ class CashTests {
|
||||
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
||||
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(
|
||||
amount = Amount(amount.quantity, token = amount.token.copy(deposit.copy(reference = OpaqueBytes.of(ref))))
|
||||
@ -59,7 +65,7 @@ class CashTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueMoney() {
|
||||
fun `issue by move`() {
|
||||
// Check we can't "move" money into existence.
|
||||
transaction {
|
||||
input { DummyState() }
|
||||
@ -68,7 +74,10 @@ class CashTests {
|
||||
|
||||
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
|
||||
// institution is allowed to issue as much cash as they want.
|
||||
transaction {
|
||||
@ -90,28 +99,41 @@ class CashTests {
|
||||
command(MINI_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateIssueRaw() {
|
||||
// Test generation works.
|
||||
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(ptx.inputStates().isEmpty())
|
||||
val s = ptx.outputStates()[0].data as Cash.State
|
||||
val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
|
||||
Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
}.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(MINI_CORP, s.deposit.party)
|
||||
assertEquals(DUMMY_PUBKEY_1, s.owner)
|
||||
assertTrue(ptx.commands()[0].value is Cash.Commands.Issue)
|
||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
||||
assertTrue(tx.commands[0].value is Cash.Commands.Issue)
|
||||
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 templatePtx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
Cash().generateIssue(templatePtx, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(templatePtx.inputStates().isEmpty())
|
||||
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0])
|
||||
val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply {
|
||||
Cash().generateIssue(this, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
}.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.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { issuerInState }
|
||||
output { inState.copy(amount = inState.amount * 2) }
|
||||
|
||||
// Move fails: not allowed to summon money.
|
||||
@ -154,11 +176,11 @@ class CashTests {
|
||||
}
|
||||
tweak {
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
@ -279,12 +301,12 @@ class CashTests {
|
||||
fun exitLedger() {
|
||||
// Single input/output straightforward case.
|
||||
transaction {
|
||||
input { inState }
|
||||
output { outState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||
input { issuerInState }
|
||||
output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||
|
||||
tweak {
|
||||
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"
|
||||
}
|
||||
|
||||
@ -293,20 +315,24 @@ class CashTests {
|
||||
this `fails with` "required com.r3corda.contracts.asset.FungibleAsset.Commands.Move command"
|
||||
|
||||
tweak {
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
this.verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exit ledger with multiple issuers`() {
|
||||
// Multi-issuer case.
|
||||
transaction {
|
||||
input { inState }
|
||||
input { inState `issued by` MINI_CORP }
|
||||
input { issuerInState }
|
||||
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 { inState.copy(amount = inState.amount - (200.DOLLARS `issued by` defaultIssuer)) }
|
||||
output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP }
|
||||
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"
|
||||
|
||||
@ -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
|
||||
fun multiIssuer() {
|
||||
transaction {
|
||||
@ -385,7 +423,7 @@ class CashTests {
|
||||
*/
|
||||
fun makeExit(amount: Amount<Currency>, corp: Party, depositRef: Byte = 1): WireTransaction {
|
||||
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()
|
||||
}
|
||||
|
||||
@ -404,9 +442,10 @@ class CashTests {
|
||||
assertEquals(WALLET[0].ref, wtx.inputs[0])
|
||||
assertEquals(0, wtx.outputs.size)
|
||||
|
||||
val expected = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
|
||||
val actual = wtx.commands.single().value
|
||||
assertEquals(expected, actual)
|
||||
val expectedMove = Cash.Commands.Move()
|
||||
val expectedExit = Cash.Commands.Exit(Amount(10000, Issued(MEGA_CORP.ref(1), USD)))
|
||||
|
||||
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.SecureHash
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.testing.*
|
||||
import com.r3corda.core.utilities.nonEmptySetOf
|
||||
import com.r3corda.core.utilities.*
|
||||
import com.r3corda.testing.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.time.Duration
|
||||
@ -119,19 +119,21 @@ class ObligationTests {
|
||||
}
|
||||
|
||||
// Test generation works.
|
||||
val ptx = TransactionType.General.Builder(DUMMY_NOTARY)
|
||||
Obligation<Currency>().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
||||
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
assertTrue(ptx.inputStates().isEmpty())
|
||||
val tx = TransactionType.General.Builder(notary = null).apply {
|
||||
Obligation<Currency>().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity,
|
||||
beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
}.toSignedTransaction().tx
|
||||
assertTrue(tx.inputs.isEmpty())
|
||||
val expected = Obligation.State(
|
||||
obligor = MINI_CORP,
|
||||
quantity = 100.DOLLARS.quantity,
|
||||
beneficiary = DUMMY_PUBKEY_1,
|
||||
template = megaCorpDollarSettlement
|
||||
)
|
||||
assertEquals(ptx.outputStates()[0].data, expected)
|
||||
assertTrue(ptx.commands()[0].value is Obligation.Commands.Issue)
|
||||
assertEquals(MINI_CORP_PUBKEY, ptx.commands()[0].signers[0])
|
||||
assertEquals(tx.outputs[0].data, expected)
|
||||
assertTrue(tx.commands[0].value is Obligation.Commands.Issue)
|
||||
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.
|
||||
transaction {
|
||||
@ -178,11 +180,11 @@ class ObligationTests {
|
||||
}
|
||||
tweak {
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ dependencies {
|
||||
|
||||
// Bring in the MockNode infrastructure for writing protocol unit tests.
|
||||
testCompile project(":node")
|
||||
testCompile project(":test-utils")
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
@ -55,6 +56,9 @@ dependencies {
|
||||
// AssertJ: for fluent assertions for testing
|
||||
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.
|
||||
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.SecureHash
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
@ -53,14 +54,16 @@ class DummyContract : Contract {
|
||||
return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owner.party.owningKey))
|
||||
}
|
||||
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey): TransactionBuilder {
|
||||
val priorState = prior.state.data
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: PublicKey): TransactionBuilder {
|
||||
require(priors.size > 0)
|
||||
val priorState = priors[0].state.data
|
||||
val (cmd, state) = priorState.withNewOwner(newOwner)
|
||||
return TransactionType.General.Builder(notary = prior.state.notary).withItems(
|
||||
/* INPUT */ prior,
|
||||
return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
|
||||
/* INPUTS */ *priors.toTypedArray(),
|
||||
/* COMMAND */ Command(cmd, priorState.owner),
|
||||
/* OUTPUT */ state
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package com.r3corda.core.contracts
|
||||
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 {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
|
@ -423,15 +423,43 @@ enum class NetType {
|
||||
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 commodityCode: String = symbol,
|
||||
val defaultFractionDigits: Int = 0) {
|
||||
companion object {
|
||||
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"))
|
||||
)
|
||||
fun getInstance(symbol: String): Commodity?
|
||||
= registry[symbol]
|
||||
fun getInstance(commodityCode: String): Commodity?
|
||||
= 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
|
||||
|
||||
import com.r3corda.core.contracts.clauses.Clause
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
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.serialization.OpaqueBytes
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
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
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
interface LinearState : ContractState {
|
||||
/** Unique thread id within the wallets of all parties */
|
||||
val thread: SecureHash
|
||||
interface LinearState: ContractState {
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@ -348,6 +375,12 @@ interface MoveCommand : CommandData {
|
||||
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. */
|
||||
data class AuthenticatedObject<out T : Any>(
|
||||
val signers: List<PublicKey>,
|
||||
|
@ -2,6 +2,8 @@ package com.r3corda.core.contracts
|
||||
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
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.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
|
||||
|
||||
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
|
||||
|
||||
/** 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)
|
||||
|
||||
val requiredKeys = getRequiredSigners(tx) + notaryKey
|
||||
val missing = requiredKeys - tx.signers
|
||||
val missing = requiredKeys - tx.mustSign
|
||||
|
||||
return missing
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package com.r3corda.core.contracts
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import java.security.PublicKey
|
||||
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.CommandData
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
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
|
||||
// "or", "and", "single" etc. composition of sets of clauses.
|
||||
interface 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
|
||||
}
|
||||
abstract class Clause<in S : ContractState, C : CommandData, in K : Any> {
|
||||
companion object {
|
||||
val log = loggerFor<Clause<*, *, *>>()
|
||||
}
|
||||
|
||||
enum class MatchBehaviour {
|
||||
CONTINUE,
|
||||
END,
|
||||
ERROR
|
||||
}
|
||||
/** Determine whether this clause runs or not */
|
||||
open val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
||||
|
||||
/**
|
||||
* 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
|
||||
* would check each of the output states that it applies to, looking for a zero amount, and throw IllegalStateException
|
||||
* 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
|
||||
* 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
|
||||
* commands that were not required (for example the Exit command for fungible assets is optional).
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
fun verify(tx: TransactionForContract,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData>
|
||||
|
||||
abstract fun verify(tx: TransactionForContract,
|
||||
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
|
||||
|
||||
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.
|
||||
*
|
||||
@ -13,27 +11,15 @@ import java.util.*
|
||||
* @param commands commands extracted from the transaction, which are relevant to the
|
||||
* clauses.
|
||||
*/
|
||||
fun verifyClauses(tx: TransactionForContract,
|
||||
clauses: List<SingleClause>,
|
||||
commands: Collection<AuthenticatedObject<CommandData>>) {
|
||||
val unmatchedCommands = ArrayList(commands.map { it.value })
|
||||
|
||||
verify@ for (clause in clauses) {
|
||||
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
|
||||
fun <C: CommandData> verifyClause(tx: TransactionForContract,
|
||||
clause: Clause<ContractState, C, Unit>,
|
||||
commands: List<AuthenticatedObject<C>>) {
|
||||
if (Clause.log.isTraceEnabled) {
|
||||
clause.getExecutionPath(commands).forEach {
|
||||
Clause.log.trace("Tx ${tx.origHash} clause: ${clause}")
|
||||
}
|
||||
}
|
||||
val matchedCommands = clause.verify(tx, tx.inputs, tx.outputs, commands, null)
|
||||
|
||||
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 java.util.*
|
||||
|
||||
interface GroupVerify<in S, in T : Any> {
|
||||
/**
|
||||
*
|
||||
* @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>
|
||||
}
|
||||
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>>
|
||||
|
||||
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 {
|
||||
abstract val clauses: List<GroupClause<S, T>>
|
||||
override val requiredCommands: Set<Class<out CommandData>>
|
||||
get() = emptySet()
|
||||
|
||||
abstract fun groupStates(tx: TransactionForContract): List<TransactionForContract.InOutGroup<S, T>>
|
||||
|
||||
override fun verify(tx: TransactionForContract, commands: Collection<AuthenticatedObject<CommandData>>): Set<CommandData> {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<C>>,
|
||||
groupingKey: Unit?): Set<C> {
|
||||
val groups = groupStates(tx)
|
||||
val matchedCommands = HashSet<CommandData>()
|
||||
val unmatchedCommands = ArrayList(commands.map { it.value })
|
||||
val matchedCommands = HashSet<C>()
|
||||
|
||||
for ((inputs, outputs, token) in groups) {
|
||||
val temp = verifyGroup(commands, inputs, outputs, token, tx, unmatchedCommands)
|
||||
matchedCommands.addAll(temp)
|
||||
unmatchedCommands.removeAll(temp)
|
||||
for ((groupInputs, groupOutputs, groupToken) in groups) {
|
||||
matchedCommands.addAll(clause.verify(tx, groupInputs, groupOutputs, commands, groupToken))
|
||||
}
|
||||
|
||||
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 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 CA_CERT_ALIAS = "CA Cert"
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.r3corda.core.node
|
||||
|
||||
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.TransactionResolutionException
|
||||
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.crypto.Party
|
||||
import com.r3corda.core.messaging.MessagingService
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.PublicKey
|
||||
@ -20,8 +21,8 @@ interface NetworkMapCache {
|
||||
val logger = LoggerFactory.getLogger(NetworkMapCache::class.java)
|
||||
}
|
||||
|
||||
enum class MapChangeType { Added, Removed }
|
||||
data class MapChange(val node: NodeInfo, val type: MapChangeType )
|
||||
enum class MapChangeType { Added, Removed, Modified }
|
||||
data class MapChange(val node: NodeInfo, val prevNodeInfo: NodeInfo?, val type: MapChangeType )
|
||||
|
||||
/** A list of nodes that advertise a network map service */
|
||||
val networkMapNodes: List<NodeInfo>
|
||||
@ -73,12 +74,12 @@ interface NetworkMapCache {
|
||||
* updates.
|
||||
*
|
||||
* @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 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.
|
||||
*/
|
||||
fun addMapService(net: MessagingService, service: NodeInfo,
|
||||
fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient,
|
||||
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.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
@ -85,13 +86,13 @@ interface WalletService {
|
||||
/**
|
||||
* 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.
|
||||
|
||||
/** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */
|
||||
@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) }
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.r3corda.core.node.services
|
||||
|
||||
import com.r3corda.core.contracts.SignedTransaction
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
|
||||
/**
|
||||
|
@ -1,26 +1,35 @@
|
||||
package com.r3corda.core.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import com.r3corda.core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
|
||||
|
||||
/**
|
||||
* The interface of [ProtocolStateMachineImpl] exposing methods and properties required by ProtocolLogic for compilation.
|
||||
*/
|
||||
interface ProtocolStateMachine<R> {
|
||||
@Suspendable
|
||||
fun <T : Any> sendAndReceive(topic: String, destination: Party, sessionIDForSend: Long, sessionIDForReceive: Long,
|
||||
payload: Any, recvType: Class<T>): UntrustworthyData<T>
|
||||
fun <T : Any> sendAndReceive(topic: String,
|
||||
destination: Party,
|
||||
sessionIDForSend: Long,
|
||||
sessionIDForReceive: Long,
|
||||
payload: Any,
|
||||
receiveType: Class<T>): UntrustworthyData<T>
|
||||
|
||||
@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
|
||||
fun send(topic: String, destination: Party, sessionID: Long, payload: Any)
|
||||
|
||||
val serviceHub: ServiceHub
|
||||
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.node.AttachmentsClassLoader
|
||||
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.NonEmptySetSerializer
|
||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||
@ -232,7 +234,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
|
||||
kryo.writeClassAndObject(output, obj.outputs)
|
||||
kryo.writeClassAndObject(output, obj.commands)
|
||||
kryo.writeClassAndObject(output, obj.notary)
|
||||
kryo.writeClassAndObject(output, obj.signers)
|
||||
kryo.writeClassAndObject(output, obj.mustSign)
|
||||
kryo.writeClassAndObject(output, obj.type)
|
||||
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.WalletService
|
||||
import com.r3corda.core.serialization.SingletonSerializeAsToken
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.r3corda.core.utilities.trace
|
||||
import rx.Observable
|
||||
@ -22,9 +23,6 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
*/
|
||||
@ThreadSafe
|
||||
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>()
|
||||
|
||||
// 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.
|
||||
*/
|
||||
override val linearHeads: Map<SecureHash, StateAndRef<LinearState>>
|
||||
override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||
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 {
|
||||
@ -78,14 +76,6 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
|
||||
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
|
||||
netDelta = walletAndNetDelta.second
|
||||
return@locked wallet
|
||||
@ -133,23 +123,4 @@ open class InMemoryWalletService(protected val services: ServiceHub) : Singleton
|
||||
|
||||
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.serialization.serialize
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
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.
|
||||
* 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.
|
||||
*
|
||||
* @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(
|
||||
protected val type: TransactionType = TransactionType.General(),
|
||||
@ -30,7 +37,6 @@ open class TransactionBuilder(
|
||||
protected val signers: MutableSet<PublicKey> = mutableSetOf(),
|
||||
protected var timestamp: Timestamp? = null) {
|
||||
|
||||
@Deprecated("use timestamp instead")
|
||||
val time: Timestamp? get() = timestamp
|
||||
|
||||
init {
|
||||
@ -91,10 +97,11 @@ open class TransactionBuilder(
|
||||
/** The signatures that have been collected so far - might be incomplete! */
|
||||
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}" }
|
||||
val data = toWireTransaction().serialize()
|
||||
addSignatureUnchecked(key.signWithECDSA(data.bits))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,13 +146,12 @@ open class TransactionBuilder(
|
||||
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
|
||||
}
|
||||
|
||||
open fun addInputState(stateAndRef: StateAndRef<*>) = addInputState(stateAndRef.ref, stateAndRef.state.notary)
|
||||
|
||||
fun addInputState(stateRef: StateRef, notary: Party) {
|
||||
open fun addInputState(stateAndRef: StateAndRef<*>) {
|
||||
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}\"." }
|
||||
signers.add(notary.owningKey)
|
||||
inputs.add(stateRef)
|
||||
inputs.add(stateAndRef.ref)
|
||||
}
|
||||
|
||||
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.
|
||||
inline fun <reified T : Any> loggerFor(): org.slf4j.Logger = LoggerFactory.getLogger(T::class.java)
|
||||
|
||||
inline fun org.slf4j.Logger.trace(msg: () -> String) {
|
||||
if (isTraceEnabled) trace(msg())
|
||||
}
|
||||
inline fun org.slf4j.Logger.trace(msg: () -> String) { 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 */
|
||||
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
|
||||
|
||||
@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)
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
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.Party
|
||||
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.protocols.ProtocolLogic
|
||||
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.UntrustworthyData
|
||||
import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
|
||||
import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
|
||||
import java.security.PublicKey
|
||||
@ -98,7 +103,7 @@ abstract class AbstractStateReplacementProtocol<T> {
|
||||
sendAndReceive<Ack>(node.identity, 0, sessionIdForReceive, handshake)
|
||||
|
||||
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!!)
|
||||
else {
|
||||
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 sessionIdForReceive: Long,
|
||||
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<Unit>() {
|
||||
@ -135,24 +140,21 @@ abstract class AbstractStateReplacementProtocol<T> {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
val proposal = receive<Proposal<T>>(sessionIdForReceive).validate { it }
|
||||
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(sessionIdForReceive)
|
||||
try {
|
||||
verifyProposal(proposal)
|
||||
verifyTx(proposal.stx)
|
||||
val stx: SignedTransaction = maybeProposal.unwrap { verifyProposal(maybeProposal).stx }
|
||||
verifyTx(stx)
|
||||
approve(stx)
|
||||
} catch(e: Exception) {
|
||||
// TODO: catch only specific exceptions. However, there are numerous validation exceptions
|
||||
// that might occur (tx validation/resolution, invalid proposal). Need to rethink how
|
||||
// we manage exceptions and maybe introduce some platform exception hierarchy
|
||||
val myIdentity = serviceHub.storageService.myLegalIdentity
|
||||
val state = proposal.stateRef
|
||||
val state = maybeProposal.unwrap { it.stateRef }
|
||||
val reason = StateReplacementRefused(myIdentity, state, e.message)
|
||||
|
||||
reject(reason)
|
||||
return
|
||||
}
|
||||
|
||||
approve(proposal.stx)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -164,7 +166,7 @@ abstract class AbstractStateReplacementProtocol<T> {
|
||||
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response)
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -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
|
||||
* 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
|
||||
private fun verifyTx(stx: SignedTransaction) {
|
||||
@ -199,7 +202,7 @@ abstract class AbstractStateReplacementProtocol<T> {
|
||||
private fun checkMySignatureRequired(tx: WireTransaction) {
|
||||
// TODO: use keys from the keyManagementService instead
|
||||
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
|
||||
|
@ -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?>>,
|
||||
requests: List<SecureHash>): List<T> =
|
||||
maybeItems.validate { response ->
|
||||
maybeItems.unwrap { response ->
|
||||
if (response.size != requests.size)
|
||||
throw BadAnswer()
|
||||
for ((index, resp) in response.withIndex()) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
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.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
|
||||
|
||||
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.transactions.SignedTransaction
|
||||
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
|
||||
|
||||
/**
|
||||
@ -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
|
||||
*/
|
||||
@Suspendable
|
||||
override fun verifyProposal(proposal: AbstractStateReplacementProtocol.Proposal<Party>) {
|
||||
val newNotary = proposal.modification
|
||||
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.identity == newNotary }
|
||||
require(isNotary) { "The proposed node $newNotary does not run a Notary service " }
|
||||
override fun verifyProposal(maybeProposal: UntrustworthyData<AbstractStateReplacementProtocol.Proposal<Party>>): AbstractStateReplacementProtocol.Proposal<Party> {
|
||||
return maybeProposal.unwrap { proposal ->
|
||||
val newNotary = proposal.modification
|
||||
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 proposedTx = proposal.stx.tx
|
||||
require(proposedTx.inputs.contains(state)) { "The proposed state $state is not in the proposed transaction inputs" }
|
||||
val state = proposal.stateRef
|
||||
val proposedTx = proposal.stx.tx
|
||||
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
|
||||
val blacklist = listOf("Evil Notary")
|
||||
require(!blacklist.contains(newNotary.name)) { "The proposed new notary $newNotary is not trusted by the party" }
|
||||
// An example requirement
|
||||
val blacklist = listOf("Evil Notary")
|
||||
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
|
||||
|
||||
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.Timestamp
|
||||
import com.r3corda.core.contracts.WireTransaction
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SignedData
|
||||
@ -72,7 +72,7 @@ object NotaryProtocol {
|
||||
private fun validateResponse(response: UntrustworthyData<Result>): Result {
|
||||
progressTracker.currentStep = VALIDATING
|
||||
|
||||
response.validate {
|
||||
response.unwrap {
|
||||
if (it.sig != null) validateSignature(it.sig, stx.txBits)
|
||||
else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
|
||||
else if (it.error == null || it.error !is NotaryError)
|
||||
@ -105,7 +105,7 @@ object NotaryProtocol {
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).validate { it }
|
||||
val (stx, reqIdentity) = receive<SignRequest>(receiveSessionID).unwrap { it }
|
||||
val wtx = stx.tx
|
||||
|
||||
val result = try {
|
||||
@ -205,5 +205,5 @@ sealed class 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 com.r3corda.core.contracts.Fix
|
||||
import com.r3corda.core.contracts.FixOf
|
||||
import com.r3corda.core.contracts.TransactionBuilder
|
||||
import com.r3corda.core.contracts.WireTransaction
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
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 resp = sendAndReceive<DigitalSignature.LegallyIdentifiable>(oracle, 0, sessionID, req)
|
||||
|
||||
return resp.validate { sig ->
|
||||
return resp.unwrap { sig ->
|
||||
check(sig.signer == oracle)
|
||||
tx.checkSignature(sig)
|
||||
sig
|
||||
@ -100,7 +100,7 @@ open class RatesFixProtocol(protected val tx: TransactionBuilder,
|
||||
// TODO: add deadline to receive
|
||||
val resp = sendAndReceive<ArrayList<Fix>>(oracle, 0, sessionID, req)
|
||||
|
||||
return resp.validate {
|
||||
return resp.unwrap {
|
||||
val fix = it.first()
|
||||
// Check the returned fix is for what we asked for.
|
||||
check(fix.of == fixOf)
|
||||
|
@ -2,13 +2,12 @@ package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
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.SecureHash
|
||||
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.*
|
||||
|
||||
// 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 {
|
||||
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()
|
||||
@ -61,7 +93,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
||||
|
||||
@Suspendable
|
||||
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
|
||||
// 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.
|
||||
//
|
||||
// If 'stx' is set, then 'wtx' is the contents (from the c'tor).
|
||||
stx?.verifySignatures()
|
||||
val wtx = stx?.verifySignatures() ?: wtx
|
||||
wtx?.let {
|
||||
fetchMissingAttachments(listOf(it))
|
||||
val ltx = it.toLedgerTransaction(serviceHub)
|
||||
@ -97,7 +129,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
||||
override val topic: String get() = throw UnsupportedOperationException()
|
||||
|
||||
@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
|
||||
// first traversal across the dependency graph.
|
||||
//
|
||||
@ -147,7 +179,7 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
||||
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.random63BitValue
|
||||
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.UntrustworthyData
|
||||
import com.r3corda.core.utilities.trace
|
||||
import java.math.BigDecimal
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
@ -98,15 +100,11 @@ object TwoPartyDealProtocol {
|
||||
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
untrustedPartialTX.validate { stx ->
|
||||
untrustedPartialTX.unwrap { stx ->
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false)
|
||||
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
|
||||
val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.identity.owningKey)
|
||||
logger.trace { "Received partially signed transaction: ${stx.id}" }
|
||||
|
||||
checkDependencies(stx)
|
||||
@ -242,7 +240,7 @@ object TwoPartyDealProtocol {
|
||||
val handshake = receive<Handshake<U>>(sessionID)
|
||||
|
||||
progressTracker.currentStep = VERIFYING
|
||||
handshake.validate {
|
||||
handshake.unwrap {
|
||||
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.
|
||||
|
||||
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 {
|
||||
|
@ -1,13 +1,12 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.core.contracts.SignedTransaction
|
||||
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.node.services.TimestampChecker
|
||||
import com.r3corda.core.node.services.UniquenessProvider
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.WireTransaction
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
@ -38,10 +37,11 @@ class ValidatingNotaryProtocol(otherSide: Party,
|
||||
}
|
||||
|
||||
private fun checkSignatures(stx: SignedTransaction) {
|
||||
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
|
||||
val missing = stx.verifySignatures(throwIfSignaturesAreMissing = false) - myKey
|
||||
|
||||
if (missing.isNotEmpty()) throw NotaryException(NotaryError.SignaturesMissing(missing.toList()))
|
||||
try {
|
||||
stx.verifySignatures(serviceHub.storageService.myLegalIdentity.owningKey)
|
||||
} catch(e: SignedTransaction.SignaturesMissingException) {
|
||||
throw NotaryException(NotaryError.SignaturesMissing(e.missing))
|
||||
}
|
||||
}
|
||||
|
||||
@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