mirror of
https://github.com/corda/corda.git
synced 2024-12-21 13:57:54 +00:00
Merge remote-tracking branch 'origin/release/os/4.6' into christians/update-fb-2020-06-12
This commit is contained in:
commit
d00dc42b18
9
.ci/dev/compatibility/DockerfileJDK11Compile
Normal file
9
.ci/dev/compatibility/DockerfileJDK11Compile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM azul/zulu-openjdk:11
|
||||
RUN apt-get update && apt-get install -y curl apt-transport-https \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg2 \
|
||||
software-properties-common \
|
||||
wget
|
||||
ARG USER="stresstester"
|
||||
RUN useradd -m ${USER}
|
38
.ci/dev/compatibility/JenkinsfileJDK11Compile
Normal file
38
.ci/dev/compatibility/JenkinsfileJDK11Compile
Normal file
@ -0,0 +1,38 @@
|
||||
@Library('corda-shared-build-pipeline-steps')
|
||||
import static com.r3.build.BuildControl.killAllExistingBuildsForJob
|
||||
|
||||
killAllExistingBuildsForJob(env.JOB_NAME, env.BUILD_NUMBER.toInteger())
|
||||
|
||||
pipeline {
|
||||
agent {
|
||||
dockerfile {
|
||||
label 'k8s'
|
||||
additionalBuildArgs "--build-arg USER=stresstester"
|
||||
filename '.ci/dev/compatibility/DockerfileJDK11Compile'
|
||||
}
|
||||
}
|
||||
options {
|
||||
timestamps()
|
||||
timeout(time: 3, unit: 'HOURS')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('JDK 11 Compile') {
|
||||
steps {
|
||||
sh "./gradlew --no-daemon -Pcompilation.allWarningsAsErrors=true -Ptests.failFast=false " +
|
||||
"-Ptests.ignoreFailures=true clean compileAll --stacktrace"
|
||||
}
|
||||
}
|
||||
stage('Deploy nodes') {
|
||||
steps {
|
||||
sh "./gradlew --no-daemon deployNodes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
cleanup {
|
||||
deleteDir() /* clean up our workspace */
|
||||
}
|
||||
}
|
||||
}
|
2
.ci/dev/nightly-regression/Jenkinsfile
vendored
2
.ci/dev/nightly-regression/Jenkinsfile
vendored
@ -31,7 +31,7 @@ pipeline {
|
||||
"-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " +
|
||||
"-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " +
|
||||
"-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" +
|
||||
" clean pushBuildImage --stacktrace"
|
||||
" clean jar deployNodes install pushBuildImage --stacktrace"
|
||||
}
|
||||
sh "kubectl auth can-i get pods"
|
||||
}
|
||||
|
32
.ci/dev/regression/Jenkinsfile
vendored
32
.ci/dev/regression/Jenkinsfile
vendored
@ -23,7 +23,7 @@ pipeline {
|
||||
"-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " +
|
||||
"-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " +
|
||||
"-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" +
|
||||
" clean pushBuildImage --stacktrace"
|
||||
" clean jar deployNodes install pushBuildImage --stacktrace"
|
||||
}
|
||||
sh "kubectl auth can-i get pods"
|
||||
}
|
||||
@ -57,36 +57,6 @@ pipeline {
|
||||
" allParallelSlowIntegrationTest --stacktrace"
|
||||
}
|
||||
}
|
||||
|
||||
stage('Generate sonarqube report') {
|
||||
steps {
|
||||
script {
|
||||
try {
|
||||
// running this step here is the only way to not majorly affect the distributed test plugin,
|
||||
// as now that neither returns build files nor runs jacoco reports
|
||||
sh "./gradlew --no-daemon build jacocoRootReport --stacktrace"
|
||||
withSonarQubeEnv('sq01') {
|
||||
sh "./gradlew --no-daemon sonarqube -x test --stacktrace"
|
||||
}
|
||||
timeout(time: 3, unit: 'MINUTES') {
|
||||
script {
|
||||
try {
|
||||
def qg = waitForQualityGate();
|
||||
if (qg.status != 'OK') {
|
||||
error "Pipeline aborted due to quality gate failure: ${qg.status}"
|
||||
}
|
||||
} catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) {
|
||||
println('No sonarqube webhook response within timeout. Please check the webhook configuration in sonarqube.')
|
||||
// continue the pipeline
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
println('Error while trying to execute sonarqube analysis, will be skipped.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
.github/workflows/check-pr-title.yml
vendored
Normal file
14
.github/workflows/check-pr-title.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: 'PR title check'
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
jobs:
|
||||
check-pr-title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: morrisoncole/pr-lint-action@v1.1.1
|
||||
with:
|
||||
title-regex: '^((CORDA|EG|ENT|INFRA)-\d+|NOTICK)(.*)'
|
||||
on-failed-regex-comment: "PR title failed to match regex -> `%regex%`"
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
@ -27,7 +27,7 @@ pipeline {
|
||||
"-Ddocker.push.password=\"\${DOCKER_PUSH_PWD}\" " +
|
||||
"-Ddocker.work.dir=\"/tmp/\${EXECUTOR_NUMBER}\" " +
|
||||
"-Ddocker.build.tag=\"\${DOCKER_TAG_TO_USE}\"" +
|
||||
" clean pushBuildImage preAllocateForAllParallelIntegrationTest preAllocateForAllParallelIntegrationTest --stacktrace"
|
||||
" clean jar deployNodes pushBuildImage preAllocateForAllParallelIntegrationTest preAllocateForAllParallelIntegrationTest --stacktrace"
|
||||
}
|
||||
sh "kubectl auth can-i get pods"
|
||||
}
|
||||
|
@ -293,4 +293,27 @@ class CordaRPCClientReconnectionTest {
|
||||
.isInstanceOf(RPCException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `rpc client does not attempt to reconnect after shutdown`() {
|
||||
driver(DriverParameters(cordappsForAllNodes = emptyList())) {
|
||||
val address = NetworkHostAndPort("localhost", portAllocator.nextPort())
|
||||
fun startNode(): NodeHandle {
|
||||
return startNode(
|
||||
providedName = CHARLIE_NAME,
|
||||
rpcUsers = listOf(CordaRPCClientTest.rpcUser),
|
||||
customOverrides = mapOf("rpcSettings.address" to address.toString())
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
val node = startNode()
|
||||
val client = CordaRPCClient(node.rpcAddress, config)
|
||||
(client.start(rpcUser.username, rpcUser.password, gracefulReconnect = gracefulReconnect)).use {
|
||||
val rpcOps = it.proxy as ReconnectingCordaRPCOps
|
||||
rpcOps.shutdown()
|
||||
// If we get here we know we're not stuck in a reconnect cycle with a node that's been shut down
|
||||
assertThat(rpcOps.reconnectingRPCConnection.isClosed())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,13 +311,18 @@ class ReconnectingCordaRPCOps private constructor(
|
||||
checkIfClosed()
|
||||
var remainingAttempts = maxNumberOfAttempts
|
||||
var lastException: Throwable? = null
|
||||
while (remainingAttempts != 0) {
|
||||
while (remainingAttempts != 0 && !reconnectingRPCConnection.isClosed()) {
|
||||
try {
|
||||
log.debug { "Invoking RPC $method..." }
|
||||
return method.invoke(reconnectingRPCConnection.proxy, *(args ?: emptyArray())).also {
|
||||
log.debug { "RPC $method invoked successfully." }
|
||||
}
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (method.name.equals("shutdown", true)) {
|
||||
log.debug("Shutdown invoked, stop reconnecting.", e)
|
||||
reconnectingRPCConnection.notifyServerAndClose()
|
||||
break
|
||||
}
|
||||
when (e.targetException) {
|
||||
is RejectedCommandException -> {
|
||||
log.warn("Node is being shutdown. Operation ${method.name} rejected. Retrying when node is up...", e)
|
||||
@ -349,6 +354,7 @@ class ReconnectingCordaRPCOps private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
if (reconnectingRPCConnection.isClosed()) return null
|
||||
throw MaxRpcRetryException(maxNumberOfAttempts, method, lastException)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1.
|
||||
errorTemplate = Version identifier {0} was not specified. Please specify a whole number starting from 1.
|
||||
shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR.
|
||||
actionsToFix = Investigate the logs to find out which version attribute was not specified, and add that version attribute to the CorDapp manifest.
|
||||
aliases = 1nskd37
|
@ -1,3 +1,3 @@
|
||||
errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1.
|
||||
errorTemplate = Version identifier {0} was not specified. Please specify a whole number starting from 1.
|
||||
shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR.
|
||||
actionsToFix = Investigate the logs to find out which version attribute was not specified, and add that version attribute to the CorDapp manifest.
|
5
docs/example-code/README.md
Normal file
5
docs/example-code/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Code examples for the documentation
|
||||
|
||||
The code-examples directory has been removed as the documentation has been moved to its own repo (see [here](../README.md)).
|
||||
|
||||
If you're looking for this code, look at the history of this document, it was introduced in the same commit as the code was removed.
|
@ -1,112 +0,0 @@
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
|
||||
compile {
|
||||
// We already have a SLF4J implementation on our runtime classpath,
|
||||
// and we don't need another one.
|
||||
exclude group: "org.apache.logging.log4j"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
java {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/java')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileTestJava.dependsOn tasks.getByPath(':node:capsule:buildCordaJAR')
|
||||
|
||||
dependencies {
|
||||
// Cordformation needs a SLF4J implementation when executing the Network
|
||||
// Bootstrapper, but Log4J doesn't shutdown completely from within Gradle.
|
||||
// Use a much simpler SLF4J implementation here instead.
|
||||
cordaRuntime "org.slf4j:slf4j-simple:$slf4j_version"
|
||||
|
||||
compile project(':core')
|
||||
compile project(':client:jfx')
|
||||
compile project(':node-driver')
|
||||
compile project(':testing:testserver')
|
||||
|
||||
testCompile project(':test-utils')
|
||||
|
||||
compile "org.graphstream:gs-core:1.3"
|
||||
compile("org.graphstream:gs-ui:1.3") {
|
||||
exclude group: "bouncycastle"
|
||||
exclude group: "junit"
|
||||
}
|
||||
|
||||
cordaRuntime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
cordaRuntime project(path: ":testing:testserver:testcapsule:", configuration: 'runtimeArtifacts')
|
||||
|
||||
// CorDapps: dependent flows and services
|
||||
compile project(':finance:contracts')
|
||||
compile project(':finance:workflows')
|
||||
}
|
||||
|
||||
mainClassName = "net.corda.docs.ClientRpcTutorialKt"
|
||||
|
||||
task getClientRpcTutorial(type: CreateStartScripts) {
|
||||
dependsOn(classes)
|
||||
mainClassName = "net.corda.docs.ClientRpcTutorialKt"
|
||||
applicationName = "client-rpc-tutorial"
|
||||
defaultJvmOpts = []
|
||||
outputDir = new File(project.buildDir, 'scripts')
|
||||
classpath = jar.outputs.files + project.configurations.runtime
|
||||
}
|
||||
|
||||
applicationDistribution.into("bin") {
|
||||
from(getClientRpcTutorial)
|
||||
fileMode = 0755
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDirs = sourceSets.integrationTest.output.classesDirs
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
node {
|
||||
name "O=Notary Service,OU=corda,L=London,C=GB"
|
||||
notary = [validating : true]
|
||||
p2pPort 10002
|
||||
rpcSettings {
|
||||
address "localhost:10003"
|
||||
adminAddress "localhost:10013"
|
||||
}
|
||||
webPort 10004
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10014']
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "O=Alice Corp,L=London,C=GB"
|
||||
p2pPort 10005
|
||||
rpcSettings {
|
||||
address "localhost:10006"
|
||||
adminAddress "localhost:10016"
|
||||
}
|
||||
webPort 10007
|
||||
extraConfig = ['h2Settings.address' : 'localhost:10017']
|
||||
cordapps = []
|
||||
rpcUsers = [
|
||||
['username' : "user",
|
||||
'password' : "password",
|
||||
'permissions' : ["StartFlow.net.corda.finance.flows.CashFlow"]]
|
||||
]
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.test;
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient;
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.core.contracts.Amount;
|
||||
import net.corda.core.contracts.Issued;
|
||||
import net.corda.core.contracts.Structures;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.node.services.Vault;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.finance.contracts.asset.Cash;
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow;
|
||||
import net.corda.finance.flows.CashPaymentFlow;
|
||||
import net.corda.testing.driver.DriverParameters;
|
||||
import net.corda.testing.driver.NodeHandle;
|
||||
import net.corda.testing.driver.NodeParameters;
|
||||
import net.corda.testing.node.User;
|
||||
import org.junit.Test;
|
||||
import rx.Observable;
|
||||
|
||||
import java.util.Currency;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static net.corda.finance.Currencies.DOLLARS;
|
||||
import static net.corda.node.services.Permissions.invokeRpc;
|
||||
import static net.corda.node.services.Permissions.startFlow;
|
||||
import static net.corda.testing.core.ExpectKt.expect;
|
||||
import static net.corda.testing.core.ExpectKt.expectEvents;
|
||||
import static net.corda.testing.core.TestConstants.ALICE_NAME;
|
||||
import static net.corda.testing.core.TestConstants.BOB_NAME;
|
||||
import static net.corda.testing.driver.Driver.driver;
|
||||
import static net.corda.testing.node.internal.InternalTestUtilsKt.FINANCE_CORDAPPS;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class JavaIntegrationTestingTutorial {
|
||||
@Test
|
||||
public void aliceBobCashExchangeExample() {
|
||||
// START 1
|
||||
driver(new DriverParameters()
|
||||
.withStartNodesInProcess(true)
|
||||
.withCordappsForAllNodes(FINANCE_CORDAPPS), dsl -> {
|
||||
|
||||
User aliceUser = new User("aliceUser", "testPassword1", new HashSet<>(asList(
|
||||
startFlow(CashIssueAndPaymentFlow.class),
|
||||
invokeRpc("vaultTrack")
|
||||
)));
|
||||
|
||||
User bobUser = new User("bobUser", "testPassword2", new HashSet<>(asList(
|
||||
startFlow(CashPaymentFlow.class),
|
||||
invokeRpc("vaultTrack")
|
||||
)));
|
||||
|
||||
try {
|
||||
List<CordaFuture<NodeHandle>> nodeHandleFutures = asList(
|
||||
dsl.startNode(new NodeParameters().withProvidedName(ALICE_NAME).withRpcUsers(singletonList(aliceUser))),
|
||||
dsl.startNode(new NodeParameters().withProvidedName(BOB_NAME).withRpcUsers(singletonList(bobUser)))
|
||||
);
|
||||
|
||||
NodeHandle alice = nodeHandleFutures.get(0).get();
|
||||
NodeHandle bob = nodeHandleFutures.get(1).get();
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
CordaRPCClient aliceClient = new CordaRPCClient(alice.getRpcAddress());
|
||||
CordaRPCOps aliceProxy = aliceClient.start("aliceUser", "testPassword1").getProxy();
|
||||
|
||||
CordaRPCClient bobClient = new CordaRPCClient(bob.getRpcAddress());
|
||||
CordaRPCOps bobProxy = bobClient.start("bobUser", "testPassword2").getProxy();
|
||||
// END 2
|
||||
|
||||
// START 3
|
||||
Observable<Vault.Update<Cash.State>> bobVaultUpdates = bobProxy.vaultTrack(Cash.State.class).getUpdates();
|
||||
Observable<Vault.Update<Cash.State>> aliceVaultUpdates = aliceProxy.vaultTrack(Cash.State.class).getUpdates();
|
||||
// END 3
|
||||
|
||||
// START 4
|
||||
OpaqueBytes issueRef = OpaqueBytes.of((byte)0);
|
||||
aliceProxy.startFlowDynamic(
|
||||
CashIssueAndPaymentFlow.class,
|
||||
DOLLARS(1000),
|
||||
issueRef,
|
||||
bob.getNodeInfo().getLegalIdentities().get(0),
|
||||
true,
|
||||
dsl.getDefaultNotaryIdentity()
|
||||
).getReturnValue().get();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<Vault.Update<Cash.State>> cashVaultUpdateClass = (Class<Vault.Update<Cash.State>>)(Class<?>)Vault.Update.class;
|
||||
|
||||
expectEvents(bobVaultUpdates, true, () ->
|
||||
expect(cashVaultUpdateClass, update -> true, update -> {
|
||||
System.out.println("Bob got vault update of " + update);
|
||||
Amount<Issued<Currency>> amount = update.getProduced().iterator().next().getState().getData().getAmount();
|
||||
assertEquals(DOLLARS(1000), Structures.withoutIssuer(amount));
|
||||
return null;
|
||||
})
|
||||
);
|
||||
// END 4
|
||||
|
||||
// START 5
|
||||
bobProxy.startFlowDynamic(
|
||||
CashPaymentFlow.class,
|
||||
DOLLARS(1000),
|
||||
alice.getNodeInfo().getLegalIdentities().get(0)
|
||||
).getReturnValue().get();
|
||||
|
||||
expectEvents(aliceVaultUpdates, true, () ->
|
||||
expect(cashVaultUpdateClass, update -> true, update -> {
|
||||
System.out.println("Alice got vault update of " + update);
|
||||
Amount<Issued<Currency>> amount = update.getProduced().iterator().next().getState().getData().getAmount();
|
||||
assertEquals(DOLLARS(1000), Structures.withoutIssuer(amount));
|
||||
return null;
|
||||
})
|
||||
);
|
||||
// END 5
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Exception thrown in driver DSL", e);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.test;
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.utilities.KotlinUtilsKt;
|
||||
import net.corda.docs.java.tutorial.flowstatemachines.ExampleSummingFlow;
|
||||
import net.corda.node.services.Permissions;
|
||||
import net.corda.testing.driver.*;
|
||||
import net.corda.testing.node.User;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import static java.util.Collections.singleton;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static net.corda.testing.core.TestConstants.ALICE_NAME;
|
||||
import static net.corda.testing.driver.Driver.driver;
|
||||
import static net.corda.testing.node.internal.InternalTestUtilsKt.cordappWithPackages;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class TutorialFlowAsyncOperationTest {
|
||||
// DOCSTART summingWorks
|
||||
@Test
|
||||
public void summingWorks() {
|
||||
driver(new DriverParameters(singletonList(cordappWithPackages("net.corda.docs.java.tutorial.flowstatemachines"))), (DriverDSL dsl) -> {
|
||||
User aliceUser = new User("aliceUser", "testPassword1", singleton(Permissions.all()));
|
||||
Future<NodeHandle> aliceFuture = dsl.startNode(new NodeParameters()
|
||||
.withProvidedName(ALICE_NAME)
|
||||
.withRpcUsers(singletonList(aliceUser))
|
||||
);
|
||||
NodeHandle alice = KotlinUtilsKt.getOrThrow(aliceFuture, null);
|
||||
CordaRPCClient aliceClient = new CordaRPCClient(alice.getRpcAddress());
|
||||
CordaRPCOps aliceProxy = aliceClient.start("aliceUser", "testPassword1").getProxy();
|
||||
Future<Integer> answerFuture = aliceProxy.startFlowDynamic(ExampleSummingFlow.class).getReturnValue();
|
||||
int answer = KotlinUtilsKt.getOrThrow(answerFuture, null);
|
||||
assertEquals(3, answer);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
// DOCEND summingWorks
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package net.corda.docs.kotlin.tutorial.test
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultTrackBy
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class KotlinIntegrationTestingTutorial {
|
||||
@Test(timeout=300_000)
|
||||
fun `alice bob cash exchange example`() {
|
||||
// START 1
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = FINANCE_CORDAPPS)) {
|
||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(
|
||||
startFlow<CashIssueAndPaymentFlow>(),
|
||||
invokeRpc("vaultTrackBy")
|
||||
))
|
||||
|
||||
val bobUser = User("bobUser", "testPassword2", permissions = setOf(
|
||||
startFlow<CashPaymentFlow>(),
|
||||
invokeRpc("vaultTrackBy")
|
||||
))
|
||||
|
||||
val (alice, bob) = listOf(
|
||||
startNode(providedName = ALICE_NAME, rpcUsers = listOf(aliceUser)),
|
||||
startNode(providedName = BOB_NAME, rpcUsers = listOf(bobUser))
|
||||
).map { it.getOrThrow() }
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
val aliceClient = CordaRPCClient(alice.rpcAddress)
|
||||
val aliceProxy: CordaRPCOps = aliceClient.start("aliceUser", "testPassword1").proxy
|
||||
|
||||
val bobClient = CordaRPCClient(bob.rpcAddress)
|
||||
val bobProxy: CordaRPCOps = bobClient.start("bobUser", "testPassword2").proxy
|
||||
// END 2
|
||||
|
||||
// START 3
|
||||
val bobVaultUpdates: Observable<Vault.Update<Cash.State>> = bobProxy.vaultTrackBy<Cash.State>().updates
|
||||
val aliceVaultUpdates: Observable<Vault.Update<Cash.State>> = aliceProxy.vaultTrackBy<Cash.State>().updates
|
||||
// END 3
|
||||
|
||||
// START 4
|
||||
val issueRef = OpaqueBytes.of(0)
|
||||
aliceProxy.startFlow(::CashIssueAndPaymentFlow,
|
||||
1000.DOLLARS,
|
||||
issueRef,
|
||||
bob.nodeInfo.singleIdentity(),
|
||||
true,
|
||||
defaultNotaryIdentity
|
||||
).returnValue.getOrThrow()
|
||||
|
||||
bobVaultUpdates.expectEvents {
|
||||
expect { update ->
|
||||
println("Bob got vault update of $update")
|
||||
val amount: Amount<Issued<Currency>> = update.produced.first().state.data.amount
|
||||
assertEquals(1000.DOLLARS, amount.withoutIssuer())
|
||||
}
|
||||
}
|
||||
// END 4
|
||||
|
||||
// START 5
|
||||
bobProxy.startFlow(::CashPaymentFlow, 1000.DOLLARS, alice.nodeInfo.singleIdentity()).returnValue.getOrThrow()
|
||||
|
||||
aliceVaultUpdates.expectEvents {
|
||||
expect { update ->
|
||||
println("Alice got vault update of $update")
|
||||
val amount: Amount<Issued<Currency>> = update.produced.first().state.data.amount
|
||||
assertEquals(1000.DOLLARS, amount.withoutIssuer())
|
||||
}
|
||||
}
|
||||
// END 5
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package net.corda.docs.kotlin.tutorial.test
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.docs.kotlin.tutorial.flowstatemachines.ExampleSummingFlow
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.internal.cordappWithPackages
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TutorialFlowAsyncOperationTest {
|
||||
// DOCSTART summingWorks
|
||||
@Test(timeout=300_000)
|
||||
fun summingWorks() {
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = listOf(cordappWithPackages("net.corda.docs.kotlin.tutorial.flowstatemachines")))) {
|
||||
val aliceUser = User("aliceUser", "testPassword1", permissions = setOf(Permissions.all()))
|
||||
val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(aliceUser)).getOrThrow()
|
||||
val aliceClient = CordaRPCClient(alice.rpcAddress)
|
||||
val aliceProxy = aliceClient.start("aliceUser", "testPassword1").proxy
|
||||
val answer = aliceProxy.startFlow(::ExampleSummingFlow).returnValue.getOrThrow()
|
||||
assertEquals(3, answer)
|
||||
}
|
||||
}
|
||||
// DOCEND summingWorks
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
// We purposefully have this template here as part of progressing through the tutorial
|
||||
package com.template;
|
||||
|
||||
import net.corda.core.contracts.CommandData;
|
||||
import net.corda.core.contracts.Contract;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class TemplateContract implements Contract {
|
||||
// This is used to identify our contract when building a transaction.
|
||||
public static final String ID = "com.template.TemplateContract";
|
||||
|
||||
/**
|
||||
* A transaction is considered valid if the verify() function of the contract of each of the transaction's input
|
||||
* and output states does not throw an exception.
|
||||
*/
|
||||
@Override
|
||||
public void verify(@NotNull LedgerTransaction tx) {}
|
||||
|
||||
public interface Commands extends CommandData {
|
||||
class Action implements Commands {}
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package net.corda.docs.java;
|
||||
|
||||
// START 1
|
||||
import net.corda.client.rpc.CordaRPCClient;
|
||||
import net.corda.client.rpc.CordaRPCConnection;
|
||||
import net.corda.core.messaging.CordaRPCOps;
|
||||
import net.corda.core.utilities.NetworkHostAndPort;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class ClientRpcExample {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClientRpcExample.class);
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 3) {
|
||||
throw new IllegalArgumentException("Usage: TemplateClient <node address> <username> <password>");
|
||||
}
|
||||
final NetworkHostAndPort nodeAddress = NetworkHostAndPort.parse(args[0]);
|
||||
String username = args[1];
|
||||
String password = args[2];
|
||||
|
||||
final CordaRPCClient client = new CordaRPCClient(nodeAddress);
|
||||
final CordaRPCConnection connection = client.start(username, password);
|
||||
final CordaRPCOps cordaRPCOperations = connection.getProxy();
|
||||
|
||||
logger.info(cordaRPCOperations.currentNodeTime().toString());
|
||||
|
||||
connection.notifyServerAndClose();
|
||||
}
|
||||
}
|
||||
// END 1
|
@ -1,137 +0,0 @@
|
||||
package net.corda.docs.java;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class FinalityFlowMigration {
|
||||
public static SignedTransaction dummyTransactionWithParticipant(Party party) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
// DOCSTART SimpleFlowUsingOldApi
|
||||
public static class SimpleFlowUsingOldApi extends FlowLogic<SignedTransaction> {
|
||||
private final Party counterparty;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public SignedTransaction call() throws FlowException {
|
||||
SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
|
||||
return subFlow(new FinalityFlow(stx));
|
||||
}
|
||||
// DOCEND SimpleFlowUsingOldApi
|
||||
|
||||
public SimpleFlowUsingOldApi(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART SimpleFlowUsingNewApi
|
||||
// Notice how the flow *must* now be an initiating flow even when it wasn't before.
|
||||
@InitiatingFlow
|
||||
public static class SimpleFlowUsingNewApi extends FlowLogic<SignedTransaction> {
|
||||
private final Party counterparty;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public SignedTransaction call() throws FlowException {
|
||||
SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
|
||||
// For each non-local participant in the transaction we must initiate a flow session with them.
|
||||
FlowSession session = initiateFlow(counterparty);
|
||||
return subFlow(new FinalityFlow(stx, session));
|
||||
}
|
||||
// DOCEND SimpleFlowUsingNewApi
|
||||
|
||||
public SimpleFlowUsingNewApi(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
}
|
||||
}
|
||||
// DOCSTART SimpleNewResponderFlow
|
||||
// All participants will run this flow to receive and record the finalised transaction into their vault.
|
||||
@InitiatedBy(SimpleFlowUsingNewApi.class)
|
||||
public static class SimpleNewResponderFlow extends FlowLogic<Void> {
|
||||
private final FlowSession otherSide;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
subFlow(new ReceiveFinalityFlow(otherSide));
|
||||
return null;
|
||||
}
|
||||
// DOCEND SimpleNewResponderFlow
|
||||
|
||||
public SimpleNewResponderFlow(FlowSession otherSide) {
|
||||
this.otherSide = otherSide;
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART ExistingInitiatingFlow
|
||||
// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
|
||||
// to allow for backwards compatibility with nodes running the old CorDapp.
|
||||
@InitiatingFlow(version = 2)
|
||||
public static class ExistingInitiatingFlow extends FlowLogic<SignedTransaction> {
|
||||
private final Party counterparty;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public SignedTransaction call() throws FlowException {
|
||||
SignedTransaction partiallySignedTx = dummyTransactionWithParticipant(counterparty);
|
||||
FlowSession session = initiateFlow(counterparty);
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(partiallySignedTx, singletonList(session)));
|
||||
// Determine which version of the flow that other side is using.
|
||||
if (session.getCounterpartyFlowInfo().getFlowVersion() == 1) {
|
||||
// Use the old API if the other side is using the previous version of the flow.
|
||||
return subFlow(new FinalityFlow(fullySignedTx));
|
||||
} else {
|
||||
// Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
|
||||
return subFlow(new FinalityFlow(fullySignedTx, session));
|
||||
}
|
||||
}
|
||||
// DOCEND ExistingInitiatingFlow
|
||||
|
||||
public ExistingInitiatingFlow(Party counterparty) {
|
||||
this.counterparty = counterparty;
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(ExistingInitiatingFlow.class)
|
||||
public static class ExistingResponderFlow extends FlowLogic<Void> {
|
||||
private final FlowSession otherSide;
|
||||
|
||||
public ExistingResponderFlow(FlowSession otherSide) {
|
||||
this.otherSide = otherSide;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
|
||||
// DOCSTART ExistingResponderFlow
|
||||
// First we have to run the SignTransactionFlow, which will return a SignedTransaction.
|
||||
SignedTransaction txWeJustSigned = subFlow(new SignTransactionFlow(otherSide) {
|
||||
@Suspendable
|
||||
@Override
|
||||
protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException {
|
||||
// Implement responder flow transaction checks here
|
||||
}
|
||||
});
|
||||
|
||||
if (otherSide.getCounterpartyFlowInfo().getFlowVersion() >= 2) {
|
||||
// The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
|
||||
// If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
|
||||
// that was just signed by passing the transaction id to ReceiveFinalityFlow.
|
||||
subFlow(new ReceiveFinalityFlow(otherSide, txWeJustSigned.getId()));
|
||||
} else {
|
||||
// Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
|
||||
// will automatically record the finalised transaction using the old insecure mechanism.
|
||||
}
|
||||
// DOCEND ExistingResponderFlow
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,689 +0,0 @@
|
||||
package net.corda.docs.java;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.crypto.TransactionSignature;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.CordaX500Name;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.identity.PartyAndCertificate;
|
||||
import net.corda.core.internal.FetchDataFlow;
|
||||
import net.corda.core.node.services.Vault;
|
||||
import net.corda.core.node.services.Vault.Page;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
import net.corda.core.utilities.ProgressTracker.Step;
|
||||
import net.corda.core.utilities.UntrustworthyData;
|
||||
import net.corda.finance.contracts.asset.Cash;
|
||||
import net.corda.testing.contracts.DummyContract;
|
||||
import net.corda.testing.contracts.DummyState;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static java.util.Collections.*;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
import static net.corda.core.crypto.Crypto.generateKeyPair;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class FlowCookbook {
|
||||
// ``InitiatorFlow`` is our first flow, and will communicate with
|
||||
// ``ResponderFlow``, below.
|
||||
// We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be
|
||||
// started directly by the node.
|
||||
@InitiatingFlow
|
||||
// We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the
|
||||
// node's owner to start the flow via RPC.
|
||||
@StartableByRPC
|
||||
// Every flow must subclass ``FlowLogic``. The generic indicates the
|
||||
// flow's return type.
|
||||
public static class InitiatorFlow extends FlowLogic<Void> {
|
||||
|
||||
private final boolean arg1;
|
||||
private final int arg2;
|
||||
private final Party counterparty;
|
||||
private final Party regulator;
|
||||
|
||||
public InitiatorFlow(boolean arg1, int arg2, Party counterparty, Party regulator) {
|
||||
this.arg1 = arg1;
|
||||
this.arg2 = arg2;
|
||||
this.counterparty = counterparty;
|
||||
this.regulator = regulator;
|
||||
}
|
||||
|
||||
/*----------------------------------
|
||||
* WIRING UP THE PROGRESS TRACKER *
|
||||
----------------------------------*/
|
||||
// Giving our flow a progress tracker allows us to see the flow's
|
||||
// progress visually in our node's CRaSH shell.
|
||||
// DOCSTART 17
|
||||
private static final Step ID_OTHER_NODES = new Step("Identifying other nodes on the network.");
|
||||
private static final Step SENDING_AND_RECEIVING_DATA = new Step("Sending data between parties.");
|
||||
private static final Step EXTRACTING_VAULT_STATES = new Step("Extracting states from the vault.");
|
||||
private static final Step OTHER_TX_COMPONENTS = new Step("Gathering a transaction's other components.");
|
||||
private static final Step TX_BUILDING = new Step("Building a transaction.");
|
||||
private static final Step TX_SIGNING = new Step("Signing a transaction.");
|
||||
private static final Step TX_VERIFICATION = new Step("Verifying a transaction.");
|
||||
private static final Step SIGS_GATHERING = new Step("Gathering a transaction's signatures.") {
|
||||
// Wiring up a child progress tracker allows us to see the
|
||||
// subflow's progress steps in our flow's progress tracker.
|
||||
@Override
|
||||
public ProgressTracker childProgressTracker() {
|
||||
return CollectSignaturesFlow.tracker();
|
||||
}
|
||||
};
|
||||
private static final Step VERIFYING_SIGS = new Step("Verifying a transaction's signatures.");
|
||||
private static final Step FINALISATION = new Step("Finalising a transaction.") {
|
||||
@Override
|
||||
public ProgressTracker childProgressTracker() {
|
||||
return FinalityFlow.tracker();
|
||||
}
|
||||
};
|
||||
|
||||
private final ProgressTracker progressTracker = new ProgressTracker(
|
||||
ID_OTHER_NODES,
|
||||
SENDING_AND_RECEIVING_DATA,
|
||||
EXTRACTING_VAULT_STATES,
|
||||
OTHER_TX_COMPONENTS,
|
||||
TX_BUILDING,
|
||||
TX_SIGNING,
|
||||
TX_VERIFICATION,
|
||||
SIGS_GATHERING,
|
||||
FINALISATION
|
||||
);
|
||||
// DOCEND 17
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// We'll be using a dummy public key for demonstration purposes.
|
||||
PublicKey dummyPubKey = generateKeyPair().getPublic();
|
||||
|
||||
/*---------------------------
|
||||
* IDENTIFYING OTHER NODES *
|
||||
---------------------------*/
|
||||
// DOCSTART 18
|
||||
progressTracker.setCurrentStep(ID_OTHER_NODES);
|
||||
// DOCEND 18
|
||||
|
||||
// Every transaction needs a notary:
|
||||
// - To prevent double-spends if the transaction has inputs
|
||||
// - To serve as a timestamping authority if the transaction has a
|
||||
// time-window
|
||||
// We retrieve a notary from the network map.
|
||||
// DOCSTART 01
|
||||
CordaX500Name notaryName = new CordaX500Name("Notary Service", "London", "GB");
|
||||
Party notary = getServiceHub().getNetworkMapCache().getNotary(notaryName);
|
||||
// DOCEND 01
|
||||
|
||||
// We may also need to identify a specific counterparty. We do so
|
||||
// using the identity service.
|
||||
// DOCSTART 02
|
||||
CordaX500Name counterPartyName = new CordaX500Name("NodeA", "London", "GB");
|
||||
Party namedCounterparty = getServiceHub().getIdentityService().wellKnownPartyFromX500Name(counterPartyName);
|
||||
Party keyedCounterparty = getServiceHub().getIdentityService().partyFromKey(dummyPubKey);
|
||||
// DOCEND 02
|
||||
|
||||
/*------------------------------
|
||||
* SENDING AND RECEIVING DATA *
|
||||
------------------------------*/
|
||||
progressTracker.setCurrentStep(SENDING_AND_RECEIVING_DATA);
|
||||
|
||||
// We start by initiating a flow session with the counterparty. We
|
||||
// will use this session to send and receive messages from the
|
||||
// counterparty.
|
||||
// DOCSTART initiateFlow
|
||||
FlowSession counterpartySession = initiateFlow(counterparty);
|
||||
// DOCEND initiateFlow
|
||||
|
||||
// We can send arbitrary data to a counterparty.
|
||||
// If this is the first ``send``, the counterparty will either:
|
||||
// 1. Ignore the message if they are not registered to respond
|
||||
// to messages from this flow.
|
||||
// 2. Start the flow they have registered to respond to this flow,
|
||||
// and run the flow until the first call to ``receive``, at
|
||||
// which point they process the message.
|
||||
// In other words, we are assuming that the counterparty is
|
||||
// registered to respond to this flow, and has a corresponding
|
||||
// ``receive`` call.
|
||||
// DOCSTART 04
|
||||
counterpartySession.send(new Object());
|
||||
// DOCEND 04
|
||||
|
||||
// We can wait to receive arbitrary data of a specific type from a
|
||||
// counterparty. Again, this implies a corresponding ``send`` call
|
||||
// in the counterparty's flow. A few scenarios:
|
||||
// - We never receive a message back. In the current design, the
|
||||
// flow is paused until the node's owner kills the flow.
|
||||
// - Instead of sending a message back, the counterparty throws a
|
||||
// ``FlowException``. This exception is propagated back to us,
|
||||
// and we can use the error message to establish what happened.
|
||||
// - We receive a message back, but it's of the wrong type. In
|
||||
// this case, a ``FlowException`` is thrown.
|
||||
// - We receive back a message of the correct type. All is good.
|
||||
//
|
||||
// Upon calling ``receive()`` (or ``sendAndReceive()``), the
|
||||
// ``FlowLogic`` is suspended until it receives a response.
|
||||
//
|
||||
// We receive the data wrapped in an ``UntrustworthyData``
|
||||
// instance. This is a reminder that the data we receive may not
|
||||
// be what it appears to be! We must unwrap the
|
||||
// ``UntrustworthyData`` using a lambda.
|
||||
// DOCSTART 05
|
||||
UntrustworthyData<Integer> packet1 = counterpartySession.receive(Integer.class);
|
||||
Integer integer = packet1.unwrap(data -> {
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
// Return the object.
|
||||
return data;
|
||||
});
|
||||
// DOCEND 05
|
||||
|
||||
// We can also use a single call to send data to a counterparty
|
||||
// and wait to receive data of a specific type back. The type of
|
||||
// data sent doesn't need to match the type of the data received
|
||||
// back.
|
||||
// DOCSTART 07
|
||||
UntrustworthyData<Boolean> packet2 = counterpartySession.sendAndReceive(Boolean.class, "You can send and receive any class!");
|
||||
Boolean bool = packet2.unwrap(data -> {
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
// Return the object.
|
||||
return data;
|
||||
});
|
||||
// DOCEND 07
|
||||
|
||||
// We're not limited to sending to and receiving from a single
|
||||
// counterparty. A flow can send messages to as many parties as it
|
||||
// likes, and each party can invoke a different response flow.
|
||||
// DOCSTART 06
|
||||
FlowSession regulatorSession = initiateFlow(regulator);
|
||||
regulatorSession.send(new Object());
|
||||
UntrustworthyData<Object> packet3 = regulatorSession.receive(Object.class);
|
||||
// DOCEND 06
|
||||
|
||||
/*------------------------------------
|
||||
* EXTRACTING STATES FROM THE VAULT *
|
||||
------------------------------------*/
|
||||
progressTracker.setCurrentStep(EXTRACTING_VAULT_STATES);
|
||||
|
||||
// Let's assume there are already some ``DummyState``s in our
|
||||
// node's vault, stored there as a result of running past flows,
|
||||
// and we want to consume them in a transaction. There are many
|
||||
// ways to extract these states from our vault.
|
||||
|
||||
// For example, we would extract any unconsumed ``DummyState``s
|
||||
// from our vault as follows:
|
||||
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED);
|
||||
Page<DummyState> results = getServiceHub().getVaultService().queryBy(DummyState.class, criteria);
|
||||
List<StateAndRef<DummyState>> dummyStates = results.getStates();
|
||||
|
||||
// For a full list of the available ways of extracting states from
|
||||
// the vault, see the Vault Query docs page.
|
||||
|
||||
// When building a transaction, input states are passed in as
|
||||
// ``StateRef`` instances, which pair the hash of the transaction
|
||||
// that generated the state with the state's index in the outputs
|
||||
// of that transaction. In practice, we'd pass the transaction hash
|
||||
// or the ``StateRef`` as a parameter to the flow, or extract the
|
||||
// ``StateRef`` from our vault.
|
||||
// DOCSTART 20
|
||||
StateRef ourStateRef = new StateRef(SecureHash.sha256("DummyTransactionHash"), 0);
|
||||
// DOCEND 20
|
||||
// A ``StateAndRef`` pairs a ``StateRef`` with the state it points to.
|
||||
// DOCSTART 21
|
||||
StateAndRef ourStateAndRef = getServiceHub().toStateAndRef(ourStateRef);
|
||||
// DOCEND 21
|
||||
|
||||
/*------------------------------------------
|
||||
* GATHERING OTHER TRANSACTION COMPONENTS *
|
||||
------------------------------------------*/
|
||||
progressTracker.setCurrentStep(OTHER_TX_COMPONENTS);
|
||||
|
||||
// Reference input states are constructed from StateAndRefs.
|
||||
// DOCSTART 55
|
||||
ReferencedStateAndRef referenceState = ourStateAndRef.referenced();
|
||||
// DOCEND 55
|
||||
// Output states are constructed from scratch.
|
||||
// DOCSTART 22
|
||||
DummyState ourOutputState = new DummyState();
|
||||
// DOCEND 22
|
||||
// Or as copies of other states with some properties changed.
|
||||
// DOCSTART 23
|
||||
DummyState ourOtherOutputState = ourOutputState.copy(77);
|
||||
// DOCEND 23
|
||||
|
||||
// We then need to pair our output state with a contract.
|
||||
// DOCSTART 47
|
||||
StateAndContract ourOutput = new StateAndContract(ourOutputState, DummyContract.PROGRAM_ID);
|
||||
// DOCEND 47
|
||||
|
||||
// Commands pair a ``CommandData`` instance with a list of
|
||||
// public keys. To be valid, the transaction requires a signature
|
||||
// matching every public key in all of the transaction's commands.
|
||||
// DOCSTART 24
|
||||
DummyContract.Commands.Create commandData = new DummyContract.Commands.Create();
|
||||
PublicKey ourPubKey = getServiceHub().getMyInfo().getLegalIdentitiesAndCerts().get(0).getOwningKey();
|
||||
PublicKey counterpartyPubKey = counterparty.getOwningKey();
|
||||
List<PublicKey> requiredSigners = ImmutableList.of(ourPubKey, counterpartyPubKey);
|
||||
Command<DummyContract.Commands.Create> ourCommand = new Command<>(commandData, requiredSigners);
|
||||
// DOCEND 24
|
||||
|
||||
// ``CommandData`` can either be:
|
||||
// 1. Of type ``TypeOnlyCommandData``, in which case it only
|
||||
// serves to attach signers to the transaction and possibly
|
||||
// fork the contract's verification logic.
|
||||
TypeOnlyCommandData typeOnlyCommandData = new DummyContract.Commands.Create();
|
||||
// 2. Include additional data which can be used by the contract
|
||||
// during verification, alongside fulfilling the roles above
|
||||
CommandData commandDataWithData = new Cash.Commands.Issue();
|
||||
|
||||
// Attachments are identified by their hash.
|
||||
// The attachment with the corresponding hash must have been
|
||||
// uploaded ahead of time via the node's RPC interface.
|
||||
// DOCSTART 25
|
||||
SecureHash ourAttachment = SecureHash.sha256("DummyAttachment");
|
||||
// DOCEND 25
|
||||
|
||||
// Time windows represent the period of time during which a
|
||||
// transaction must be notarised. They can have a start and an end
|
||||
// time, or be open at either end.
|
||||
// DOCSTART 26
|
||||
TimeWindow ourTimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX);
|
||||
TimeWindow ourAfter = TimeWindow.fromOnly(Instant.MIN);
|
||||
TimeWindow ourBefore = TimeWindow.untilOnly(Instant.MAX);
|
||||
// DOCEND 26
|
||||
|
||||
// We can also define a time window as an ``Instant`` +/- a time
|
||||
// tolerance (e.g. 30 seconds):
|
||||
// DOCSTART 42
|
||||
TimeWindow ourTimeWindow2 = TimeWindow.withTolerance(getServiceHub().getClock().instant(), Duration.ofSeconds(30));
|
||||
// DOCEND 42
|
||||
// Or as a start-time plus a duration:
|
||||
// DOCSTART 43
|
||||
TimeWindow ourTimeWindow3 = TimeWindow.fromStartAndDuration(getServiceHub().getClock().instant(), Duration.ofSeconds(30));
|
||||
// DOCEND 43
|
||||
|
||||
/*------------------------
|
||||
* TRANSACTION BUILDING *
|
||||
------------------------*/
|
||||
progressTracker.setCurrentStep(TX_BUILDING);
|
||||
|
||||
// If our transaction has input states or a time-window, we must instantiate it with a
|
||||
// notary.
|
||||
// DOCSTART 19
|
||||
TransactionBuilder txBuilder = new TransactionBuilder(notary);
|
||||
// DOCEND 19
|
||||
|
||||
// Otherwise, we can choose to instantiate it without one:
|
||||
// DOCSTART 46
|
||||
TransactionBuilder txBuilderNoNotary = new TransactionBuilder();
|
||||
// DOCEND 46
|
||||
|
||||
// We add items to the transaction builder using ``TransactionBuilder.withItems``:
|
||||
// DOCSTART 27
|
||||
txBuilder.withItems(
|
||||
// Inputs, as ``StateAndRef``s that reference to the outputs of previous transactions
|
||||
ourStateAndRef,
|
||||
// Outputs, as ``StateAndContract``s
|
||||
ourOutput,
|
||||
// Commands, as ``Command``s
|
||||
ourCommand,
|
||||
// Attachments, as ``SecureHash``es
|
||||
ourAttachment,
|
||||
// A time-window, as ``TimeWindow``
|
||||
ourTimeWindow
|
||||
);
|
||||
// DOCEND 27
|
||||
|
||||
// We can also add items using methods for the individual components.
|
||||
|
||||
// The individual methods for adding input states and attachments:
|
||||
// DOCSTART 28
|
||||
txBuilder.addInputState(ourStateAndRef);
|
||||
txBuilder.addAttachment(ourAttachment);
|
||||
// DOCEND 28
|
||||
|
||||
// An output state can be added as a ``ContractState``, contract class name and notary.
|
||||
// DOCSTART 49
|
||||
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, notary);
|
||||
// DOCEND 49
|
||||
// We can also leave the notary field blank, in which case the transaction's default
|
||||
// notary is used.
|
||||
// DOCSTART 50
|
||||
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID);
|
||||
// DOCEND 50
|
||||
// Or we can add the output state as a ``TransactionState``, which already specifies
|
||||
// the output's contract and notary.
|
||||
// DOCSTART 51
|
||||
TransactionState txState = new TransactionState(ourOutputState, DummyContract.PROGRAM_ID, notary);
|
||||
// DOCEND 51
|
||||
|
||||
// Commands can be added as ``Command``s.
|
||||
// DOCSTART 52
|
||||
txBuilder.addCommand(ourCommand);
|
||||
// DOCEND 52
|
||||
// Or as ``CommandData`` and a ``vararg PublicKey``.
|
||||
// DOCSTART 53
|
||||
txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey);
|
||||
// DOCEND 53
|
||||
|
||||
// We can set a time-window directly.
|
||||
// DOCSTART 44
|
||||
txBuilder.setTimeWindow(ourTimeWindow);
|
||||
// DOCEND 44
|
||||
// Or as a start time plus a duration (e.g. 45 seconds).
|
||||
// DOCSTART 45
|
||||
txBuilder.setTimeWindow(getServiceHub().getClock().instant(), Duration.ofSeconds(45));
|
||||
// DOCEND 45
|
||||
|
||||
/*-----------------------
|
||||
* TRANSACTION SIGNING *
|
||||
-----------------------*/
|
||||
progressTracker.setCurrentStep(TX_SIGNING);
|
||||
|
||||
// We finalise the transaction by signing it,
|
||||
// converting it into a ``SignedTransaction``.
|
||||
// DOCSTART 29
|
||||
SignedTransaction onceSignedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
// DOCEND 29
|
||||
// We can also sign the transaction using a different public key:
|
||||
// DOCSTART 30
|
||||
PartyAndCertificate otherIdentity = getServiceHub().getKeyManagementService().freshKeyAndCert(getOurIdentityAndCert(), false);
|
||||
SignedTransaction onceSignedTx2 = getServiceHub().signInitialTransaction(txBuilder, otherIdentity.getOwningKey());
|
||||
// DOCEND 30
|
||||
|
||||
// If instead this was a ``SignedTransaction`` that we'd received
|
||||
// from a counterparty and we needed to sign it, we would add our
|
||||
// signature using:
|
||||
// DOCSTART 38
|
||||
SignedTransaction twiceSignedTx = getServiceHub().addSignature(onceSignedTx);
|
||||
// DOCEND 38
|
||||
// Or, if we wanted to use a different public key:
|
||||
PartyAndCertificate otherIdentity2 = getServiceHub().getKeyManagementService().freshKeyAndCert(getOurIdentityAndCert(), false);
|
||||
// DOCSTART 39
|
||||
SignedTransaction twiceSignedTx2 = getServiceHub().addSignature(onceSignedTx, otherIdentity2.getOwningKey());
|
||||
// DOCEND 39
|
||||
|
||||
// We can also generate a signature over the transaction without
|
||||
// adding it to the transaction itself. We may do this when
|
||||
// sending just the signature in a flow instead of returning the
|
||||
// entire transaction with our signature. This way, the receiving
|
||||
// node does not need to check we haven't changed anything in the
|
||||
// transaction.
|
||||
// DOCSTART 40
|
||||
TransactionSignature sig = getServiceHub().createSignature(onceSignedTx);
|
||||
// DOCEND 40
|
||||
// And again, if we wanted to use a different public key:
|
||||
// DOCSTART 41
|
||||
TransactionSignature sig2 = getServiceHub().createSignature(onceSignedTx, otherIdentity2.getOwningKey());
|
||||
// DOCEND 41
|
||||
|
||||
/*----------------------------
|
||||
* TRANSACTION VERIFICATION *
|
||||
----------------------------*/
|
||||
progressTracker.setCurrentStep(TX_VERIFICATION);
|
||||
|
||||
// Verifying a transaction will also verify every transaction in
|
||||
// the transaction's dependency chain, which will require
|
||||
// transaction data access on counterparty's node. The
|
||||
// ``SendTransactionFlow`` can be used to automate the sending and
|
||||
// data vending process. The ``SendTransactionFlow`` will listen
|
||||
// for data request until the transaction is resolved and verified
|
||||
// on the other side:
|
||||
// DOCSTART 12
|
||||
subFlow(new SendTransactionFlow(counterpartySession, twiceSignedTx));
|
||||
|
||||
// Optional request verification to further restrict data access.
|
||||
subFlow(new SendTransactionFlow(counterpartySession, twiceSignedTx) {
|
||||
@Override
|
||||
protected void verifyDataRequest(@NotNull FetchDataFlow.Request.Data dataRequest) {
|
||||
// Extra request verification.
|
||||
}
|
||||
});
|
||||
// DOCEND 12
|
||||
|
||||
// We can receive the transaction using ``ReceiveTransactionFlow``,
|
||||
// which will automatically download all the dependencies and verify
|
||||
// the transaction and then record in our vault
|
||||
// DOCSTART 13
|
||||
SignedTransaction verifiedTransaction = subFlow(new ReceiveTransactionFlow(counterpartySession));
|
||||
// DOCEND 13
|
||||
|
||||
// We can also send and receive a `StateAndRef` dependency chain and automatically resolve its dependencies.
|
||||
// DOCSTART 14
|
||||
subFlow(new SendStateAndRefFlow(counterpartySession, dummyStates));
|
||||
|
||||
// On the receive side ...
|
||||
List<StateAndRef<DummyState>> resolvedStateAndRef = subFlow(new ReceiveStateAndRefFlow<>(counterpartySession));
|
||||
// DOCEND 14
|
||||
|
||||
try {
|
||||
|
||||
// We can now verify the transaction to ensure that it satisfies
|
||||
// the contracts of all the transaction's input and output states.
|
||||
// DOCSTART 33
|
||||
twiceSignedTx.verify(getServiceHub());
|
||||
// DOCEND 33
|
||||
|
||||
// We'll often want to perform our own additional verification
|
||||
// too. Just because a transaction is valid based on the contract
|
||||
// rules and requires our signature doesn't mean we have to
|
||||
// sign it! We need to make sure the transaction represents an
|
||||
// agreement we actually want to enter into.
|
||||
|
||||
// To do this, we need to convert our ``SignedTransaction``
|
||||
// into a ``LedgerTransaction``. This will use our ServiceHub
|
||||
// to resolve the transaction's inputs and attachments into
|
||||
// actual objects, rather than just references.
|
||||
// DOCSTART 32
|
||||
LedgerTransaction ledgerTx = twiceSignedTx.toLedgerTransaction(getServiceHub());
|
||||
// DOCEND 32
|
||||
|
||||
// We can now perform our additional verification.
|
||||
// DOCSTART 34
|
||||
DummyState outputState = ledgerTx.outputsOfType(DummyState.class).get(0);
|
||||
if (outputState.getMagicNumber() != 777) {
|
||||
// ``FlowException`` is a special exception type. It will be
|
||||
// propagated back to any counterparty flows waiting for a
|
||||
// message from this flow, notifying them that the flow has
|
||||
// failed.
|
||||
throw new FlowException("We expected a magic number of 777.");
|
||||
}
|
||||
// DOCEND 34
|
||||
|
||||
} catch (GeneralSecurityException e) {
|
||||
// Handle this as required.
|
||||
}
|
||||
|
||||
// Of course, if you are not a required signer on the transaction,
|
||||
// you have no power to decide whether it is valid or not. If it
|
||||
// requires signatures from all the required signers and is
|
||||
// contractually valid, it's a valid ledger update.
|
||||
|
||||
/*------------------------
|
||||
* GATHERING SIGNATURES *
|
||||
------------------------*/
|
||||
progressTracker.setCurrentStep(SIGS_GATHERING);
|
||||
|
||||
// The list of parties who need to sign a transaction is dictated
|
||||
// by the transaction's commands. Once we've signed a transaction
|
||||
// ourselves, we can automatically gather the signatures of the
|
||||
// other required signers using ``CollectSignaturesFlow``.
|
||||
// The responder flow will need to call ``SignTransactionFlow``.
|
||||
// DOCSTART 15
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(twiceSignedTx, emptySet(), SIGS_GATHERING.childProgressTracker()));
|
||||
// DOCEND 15
|
||||
|
||||
/*------------------------
|
||||
* VERIFYING SIGNATURES *
|
||||
------------------------*/
|
||||
progressTracker.setCurrentStep(VERIFYING_SIGS);
|
||||
|
||||
try {
|
||||
|
||||
// We can verify that a transaction has all the required
|
||||
// signatures, and that they're all valid, by running:
|
||||
// DOCSTART 35
|
||||
fullySignedTx.verifyRequiredSignatures();
|
||||
// DOCEND 35
|
||||
|
||||
// If the transaction is only partially signed, we have to pass in
|
||||
// a vararg of the public keys corresponding to the missing
|
||||
// signatures, explicitly telling the system not to check them.
|
||||
// DOCSTART 36
|
||||
onceSignedTx.verifySignaturesExcept(counterpartyPubKey);
|
||||
// DOCEND 36
|
||||
|
||||
// There is also an overload of ``verifySignaturesExcept`` which accepts
|
||||
// a ``Collection`` of the public keys corresponding to the missing
|
||||
// signatures. In the example below, we could also use
|
||||
// ``Arrays.asList(counterpartyPubKey)`` instead of
|
||||
// ``Collections.singletonList(counterpartyPubKey)``.
|
||||
// DOCSTART 54
|
||||
onceSignedTx.verifySignaturesExcept(singletonList(counterpartyPubKey));
|
||||
// DOCEND 54
|
||||
|
||||
// We can also choose to only check the signatures that are
|
||||
// present. BE VERY CAREFUL - this function provides no guarantees
|
||||
// that the signatures are correct, or that none are missing.
|
||||
// DOCSTART 37
|
||||
twiceSignedTx.checkSignaturesAreValid();
|
||||
// DOCEND 37
|
||||
} catch (GeneralSecurityException e) {
|
||||
// Handle this as required.
|
||||
}
|
||||
|
||||
/*------------------------------
|
||||
* FINALISING THE TRANSACTION *
|
||||
------------------------------*/
|
||||
progressTracker.setCurrentStep(FINALISATION);
|
||||
|
||||
// We notarise the transaction and get it recorded in the vault of
|
||||
// the participants of all the transaction's states.
|
||||
// DOCSTART 09
|
||||
SignedTransaction notarisedTx1 = subFlow(new FinalityFlow(fullySignedTx, singleton(counterpartySession), FINALISATION.childProgressTracker()));
|
||||
// DOCEND 09
|
||||
// We can also choose to send it to additional parties who aren't one
|
||||
// of the state's participants.
|
||||
// DOCSTART 10
|
||||
List<FlowSession> partySessions = Arrays.asList(counterpartySession, initiateFlow(regulator));
|
||||
SignedTransaction notarisedTx2 = subFlow(new FinalityFlow(fullySignedTx, partySessions, FINALISATION.childProgressTracker()));
|
||||
// DOCEND 10
|
||||
|
||||
// DOCSTART FlowSession porting
|
||||
send(regulator, new Object()); // Old API
|
||||
// becomes
|
||||
FlowSession session = initiateFlow(regulator);
|
||||
session.send(new Object());
|
||||
// DOCEND FlowSession porting
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ``ResponderFlow`` is our second flow, and will communicate with
|
||||
// ``InitiatorFlow``.
|
||||
// We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it
|
||||
// can only be started in response to a message from its initiating flow.
|
||||
// That's ``InitiatorFlow`` in this case.
|
||||
// Each node also has several flow pairs registered by default - see
|
||||
// ``AbstractNode.installCoreFlows``.
|
||||
@InitiatedBy(InitiatorFlow.class)
|
||||
public static class ResponderFlow extends FlowLogic<Void> {
|
||||
|
||||
private final FlowSession counterpartySession;
|
||||
|
||||
public ResponderFlow(FlowSession counterpartySession) {
|
||||
this.counterpartySession = counterpartySession;
|
||||
}
|
||||
|
||||
private static final Step RECEIVING_AND_SENDING_DATA = new Step("Sending data between parties.");
|
||||
private static final Step SIGNING = new Step("Responding to CollectSignaturesFlow.");
|
||||
private static final Step FINALISATION = new Step("Finalising a transaction.");
|
||||
|
||||
private final ProgressTracker progressTracker = new ProgressTracker(
|
||||
RECEIVING_AND_SENDING_DATA,
|
||||
SIGNING,
|
||||
FINALISATION
|
||||
);
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// The ``ResponderFlow` has all the same APIs available. It looks
|
||||
// up network information, sends and receives data, and constructs
|
||||
// transactions in exactly the same way.
|
||||
|
||||
/*------------------------------
|
||||
* SENDING AND RECEIVING DATA *
|
||||
-----------------------------*/
|
||||
progressTracker.setCurrentStep(RECEIVING_AND_SENDING_DATA);
|
||||
|
||||
// We need to respond to the messages sent by the initiator:
|
||||
// 1. They sent us an ``Object`` instance
|
||||
// 2. They waited to receive an ``Integer`` instance back
|
||||
// 3. They sent a ``String`` instance and waited to receive a
|
||||
// ``Boolean`` instance back
|
||||
// Our side of the flow must mirror these calls.
|
||||
// DOCSTART 08
|
||||
Object obj = counterpartySession.receive(Object.class).unwrap(data -> data);
|
||||
String string = counterpartySession.sendAndReceive(String.class, 99).unwrap(data -> data);
|
||||
counterpartySession.send(true);
|
||||
// DOCEND 08
|
||||
|
||||
/*-----------------------------------------
|
||||
* RESPONDING TO COLLECT_SIGNATURES_FLOW *
|
||||
-----------------------------------------*/
|
||||
progressTracker.setCurrentStep(SIGNING);
|
||||
|
||||
// The responder will often need to respond to a call to
|
||||
// ``CollectSignaturesFlow``. It does so my invoking its own
|
||||
// ``SignTransactionFlow`` subclass.
|
||||
// DOCSTART 16
|
||||
class SignTxFlow extends SignTransactionFlow {
|
||||
private SignTxFlow(FlowSession otherSession, ProgressTracker progressTracker) {
|
||||
super(otherSession, progressTracker);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTransaction(SignedTransaction stx) {
|
||||
requireThat(require -> {
|
||||
// Any additional checking we see fit...
|
||||
DummyState outputState = (DummyState) stx.getTx().getOutputs().get(0).getData();
|
||||
checkArgument(outputState.getMagicNumber() == 777);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SecureHash idOfTxWeSigned = subFlow(new SignTxFlow(counterpartySession, SignTransactionFlow.tracker())).getId();
|
||||
// DOCEND 16
|
||||
|
||||
/*------------------------------
|
||||
* FINALISING THE TRANSACTION *
|
||||
------------------------------*/
|
||||
progressTracker.setCurrentStep(FINALISATION);
|
||||
|
||||
// As the final step the responder waits to receive the notarised transaction from the sending party
|
||||
// Since it knows the ID of the transaction it just signed, the transaction ID is specified to ensure the correct
|
||||
// transaction is received and recorded.
|
||||
// DOCSTART ReceiveFinalityFlow
|
||||
subFlow(new ReceiveFinalityFlow(counterpartySession, idOfTxWeSigned));
|
||||
// DOCEND ReceiveFinalityFlow
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
package net.corda.docs.java;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
// DOCSTART LaunchSpaceshipFlow
|
||||
@InitiatingFlow
|
||||
class LaunchSpaceshipFlow extends FlowLogic<Void> {
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
boolean shouldLaunchSpaceship = receive(Boolean.class, getPresident()).unwrap(s -> s);
|
||||
if (shouldLaunchSpaceship) {
|
||||
launchSpaceship();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void launchSpaceship() {
|
||||
}
|
||||
|
||||
public Party getPresident() {
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(LaunchSpaceshipFlow.class)
|
||||
@InitiatingFlow
|
||||
class PresidentSpaceshipFlow extends FlowLogic<Void> {
|
||||
private final Party launcher;
|
||||
|
||||
public PresidentSpaceshipFlow(Party launcher) {
|
||||
this.launcher = launcher;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() {
|
||||
boolean needCoffee = true;
|
||||
send(getSecretary(), needCoffee);
|
||||
boolean shouldLaunchSpaceship = false;
|
||||
send(launcher, shouldLaunchSpaceship);
|
||||
return null;
|
||||
}
|
||||
|
||||
public Party getSecretary() {
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(PresidentSpaceshipFlow.class)
|
||||
class SecretaryFlow extends FlowLogic<Void> {
|
||||
private final Party president;
|
||||
|
||||
public SecretaryFlow(Party president) {
|
||||
this.president = president;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() {
|
||||
// ignore
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DOCEND LaunchSpaceshipFlow
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
// DOCSTART LaunchSpaceshipFlowCorrect
|
||||
@InitiatingFlow
|
||||
class LaunchSpaceshipFlowCorrect extends FlowLogic<Void> {
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
FlowSession presidentSession = initiateFlow(getPresident());
|
||||
boolean shouldLaunchSpaceship = presidentSession.receive(Boolean.class).unwrap(s -> s);
|
||||
if (shouldLaunchSpaceship) {
|
||||
launchSpaceship();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void launchSpaceship() {
|
||||
}
|
||||
|
||||
public Party getPresident() {
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(LaunchSpaceshipFlowCorrect.class)
|
||||
@InitiatingFlow
|
||||
class PresidentSpaceshipFlowCorrect extends FlowLogic<Void> {
|
||||
private final FlowSession launcherSession;
|
||||
|
||||
public PresidentSpaceshipFlowCorrect(FlowSession launcherSession) {
|
||||
this.launcherSession = launcherSession;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() {
|
||||
boolean needCoffee = true;
|
||||
FlowSession secretarySession = initiateFlow(getSecretary());
|
||||
secretarySession.send(needCoffee);
|
||||
boolean shouldLaunchSpaceship = false;
|
||||
launcherSession.send(shouldLaunchSpaceship);
|
||||
return null;
|
||||
}
|
||||
|
||||
public Party getSecretary() {
|
||||
throw new AbstractMethodError();
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(PresidentSpaceshipFlowCorrect.class)
|
||||
class SecretaryFlowCorrect extends FlowLogic<Void> {
|
||||
private final FlowSession presidentSession;
|
||||
|
||||
public SecretaryFlowCorrect(FlowSession presidentSession) {
|
||||
this.presidentSession = presidentSession;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() {
|
||||
// ignore
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DOCEND LaunchSpaceshipFlowCorrect
|
@ -1,35 +0,0 @@
|
||||
package net.corda.docs.java;
|
||||
|
||||
// DOCSTART 1
|
||||
import net.corda.core.identity.CordaX500Name;
|
||||
import net.corda.testing.node.MockNetwork;
|
||||
import net.corda.testing.node.MockNetworkParameters;
|
||||
import net.corda.testing.node.StartedMockNode;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static net.corda.testing.node.TestCordapp.findCordapp;
|
||||
|
||||
public class MockNetworkTestsTutorial {
|
||||
|
||||
private final MockNetwork mockNet = new MockNetwork(new MockNetworkParameters(singletonList(findCordapp("com.mycordapp.package"))));
|
||||
|
||||
@After
|
||||
public void cleanUp() {
|
||||
mockNet.stopNodes();
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
// DOCSTART 2
|
||||
private StartedMockNode nodeA;
|
||||
private StartedMockNode nodeB;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
nodeA = mockNet.createNode();
|
||||
// We can optionally give the node a name.
|
||||
nodeB = mockNet.createNode(new CordaX500Name("Bank B", "London", "GB"));
|
||||
}
|
||||
// DOCEND 2
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.contract;
|
||||
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
import net.corda.core.transactions.LedgerTransaction.InOutGroup;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
import static net.corda.finance.contracts.utils.StateSumming.sumCashBy;
|
||||
|
||||
public class CommercialPaper implements Contract {
|
||||
// DOCSTART 1
|
||||
public static final String IOU_CONTRACT_ID = "com.example.contract.IOUContract";
|
||||
// DOCEND 1
|
||||
|
||||
// DOCSTART 3
|
||||
@Override
|
||||
public void verify(LedgerTransaction tx) {
|
||||
List<InOutGroup<State, State>> groups = tx.groupStates(State.class, State::withoutOwner);
|
||||
CommandWithParties<Commands> cmd = requireSingleCommand(tx.getCommands(), Commands.class);
|
||||
// DOCEND 3
|
||||
|
||||
// DOCSTART 4
|
||||
TimeWindow timeWindow = tx.getTimeWindow();
|
||||
|
||||
for (InOutGroup group : groups) {
|
||||
List<State> inputs = group.getInputs();
|
||||
List<State> outputs = group.getOutputs();
|
||||
|
||||
if (cmd.getValue() instanceof Commands.Move) {
|
||||
State input = inputs.get(0);
|
||||
requireThat(require -> {
|
||||
require.using("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner().getOwningKey()));
|
||||
require.using("the state is propagated", outputs.size() == 1);
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
return null;
|
||||
});
|
||||
|
||||
} else if (cmd.getValue() instanceof Commands.Redeem) {
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
State input = inputs.get(0);
|
||||
Amount<Issued<Currency>> received = sumCashBy(tx.getOutputStates(), input.getOwner());
|
||||
if (timeWindow == null) throw new IllegalArgumentException("Redemptions must be timestamped");
|
||||
Instant time = timeWindow.getFromTime();
|
||||
requireThat(require -> {
|
||||
require.using("the paper must have matured", time.isAfter(input.getMaturityDate()));
|
||||
require.using("the received amount equals the face value", received == input.getFaceValue());
|
||||
require.using("the paper must be destroyed", outputs.isEmpty());
|
||||
require.using("the transaction is signed by the owner of the CP", cmd.getSigners().contains(input.getOwner().getOwningKey()));
|
||||
return null;
|
||||
});
|
||||
} else if (cmd.getValue() instanceof Commands.Issue) {
|
||||
State output = outputs.get(0);
|
||||
if (timeWindow == null) throw new IllegalArgumentException("Issuances must have a time-window");
|
||||
Instant time = timeWindow.getUntilTime();
|
||||
requireThat(require -> {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
require.using("output states are issued by a command signer", cmd.getSigners().contains(output.getIssuance().getParty().getOwningKey()));
|
||||
require.using("output values sum to more than the inputs", output.getFaceValue().getQuantity() > 0);
|
||||
require.using("the maturity date is not in the past", time.isBefore(output.getMaturityDate()));
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
require.using("can't reissue an existing state", inputs.isEmpty());
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unrecognised command");
|
||||
}
|
||||
}
|
||||
// DOCEND 4
|
||||
}
|
||||
|
||||
// DOCSTART 2
|
||||
public static class Commands implements CommandData {
|
||||
public static class Move extends Commands {
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Move;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Redeem extends Commands {
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Redeem;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Issue extends Commands {
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Issue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 2
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.contract;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.NullKeys;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.AnonymousParty;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
|
||||
// DOCSTART 1
|
||||
public class State implements OwnableState {
|
||||
private PartyAndReference issuance;
|
||||
private AbstractParty owner;
|
||||
private Amount<Issued<Currency>> faceValue;
|
||||
private Instant maturityDate;
|
||||
|
||||
public State() {
|
||||
} // For serialization
|
||||
|
||||
public State(PartyAndReference issuance, AbstractParty owner, Amount<Issued<Currency>> faceValue,
|
||||
Instant maturityDate) {
|
||||
this.issuance = issuance;
|
||||
this.owner = owner;
|
||||
this.faceValue = faceValue;
|
||||
this.maturityDate = maturityDate;
|
||||
}
|
||||
|
||||
public State copy() {
|
||||
return new State(this.issuance, this.owner, this.faceValue, this.maturityDate);
|
||||
}
|
||||
|
||||
public State withoutOwner() {
|
||||
return new State(this.issuance, new AnonymousParty(NullKeys.NullPublicKey.INSTANCE), this.faceValue, this.maturityDate);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CommandAndState withNewOwner(@NotNull AbstractParty newOwner) {
|
||||
return new CommandAndState(new CommercialPaper.Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate));
|
||||
}
|
||||
|
||||
public PartyAndReference getIssuance() {
|
||||
return issuance;
|
||||
}
|
||||
|
||||
public AbstractParty getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public Amount<Issued<Currency>> getFaceValue() {
|
||||
return faceValue;
|
||||
}
|
||||
|
||||
public Instant getMaturityDate() {
|
||||
return maturityDate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
State state = (State) o;
|
||||
|
||||
if (issuance != null ? !issuance.equals(state.issuance) : state.issuance != null) return false;
|
||||
if (owner != null ? !owner.equals(state.owner) : state.owner != null) return false;
|
||||
if (faceValue != null ? !faceValue.equals(state.faceValue) : state.faceValue != null) return false;
|
||||
return !(maturityDate != null ? !maturityDate.equals(state.maturityDate) : state.maturityDate != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = issuance != null ? issuance.hashCode() : 0;
|
||||
result = 31 * result + (owner != null ? owner.hashCode() : 0);
|
||||
result = 31 * result + (faceValue != null ? faceValue.hashCode() : 0);
|
||||
result = 31 * result + (maturityDate != null ? maturityDate.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return ImmutableList.of(this.owner);
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
@ -1,19 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.flowstatemachines;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.flows.FlowLogic;
|
||||
import net.corda.core.flows.StartableByRPC;
|
||||
import net.corda.core.internal.FlowAsyncOperationKt;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
// DOCSTART ExampleSummingFlow
|
||||
@StartableByRPC
|
||||
public final class ExampleSummingFlow extends FlowLogic<Integer> {
|
||||
@Suspendable
|
||||
@NotNull
|
||||
@Override
|
||||
public Integer call() {
|
||||
return FlowAsyncOperationKt.executeAsync(this, new SummingOperation(1, 2), false);
|
||||
}
|
||||
}
|
||||
// DOCEND ExampleSummingFlow
|
@ -1,32 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.flowstatemachines;
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.core.internal.FlowAsyncOperation;
|
||||
import net.corda.core.internal.concurrent.CordaFutureImplKt;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
// DOCSTART SummingOperation
|
||||
public final class SummingOperation implements FlowAsyncOperation<Integer> {
|
||||
private final int a;
|
||||
private final int b;
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CordaFuture<Integer> execute(String deduplicationId) {
|
||||
return CordaFutureImplKt.doneFuture(this.a + this.b);
|
||||
}
|
||||
|
||||
public final int getA() {
|
||||
return this.a;
|
||||
}
|
||||
|
||||
public final int getB() {
|
||||
return this.b;
|
||||
}
|
||||
|
||||
public SummingOperation(int a, int b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
// DOCEND SummingOperation
|
@ -1,31 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.flowstatemachines;
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture;
|
||||
import net.corda.core.internal.FlowAsyncOperation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
// DOCSTART SummingOperationThrowing
|
||||
public final class SummingOperationThrowing implements FlowAsyncOperation<Integer> {
|
||||
private final int a;
|
||||
private final int b;
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CordaFuture<Integer> execute(String deduplicationId) {
|
||||
throw new IllegalStateException("You shouldn't be calling me");
|
||||
}
|
||||
|
||||
public final int getA() {
|
||||
return this.a;
|
||||
}
|
||||
|
||||
public final int getB() {
|
||||
return this.b;
|
||||
}
|
||||
|
||||
public SummingOperationThrowing(int a, int b) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
}
|
||||
}
|
||||
// DOCEND SummingOperationThrowing
|
@ -1,39 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.flowstatemachines;
|
||||
|
||||
import net.corda.core.flows.SignTransactionFlow;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
public class TutorialFlowStateMachines {
|
||||
// DOCSTART 1
|
||||
private final ProgressTracker progressTracker = new ProgressTracker(
|
||||
RECEIVING,
|
||||
VERIFYING,
|
||||
SIGNING,
|
||||
COLLECTING_SIGNATURES,
|
||||
RECORDING
|
||||
);
|
||||
|
||||
private static final ProgressTracker.Step RECEIVING = new ProgressTracker.Step(
|
||||
"Waiting for seller trading info");
|
||||
private static final ProgressTracker.Step VERIFYING = new ProgressTracker.Step(
|
||||
"Verifying seller assets");
|
||||
private static final ProgressTracker.Step SIGNING = new ProgressTracker.Step(
|
||||
"Generating and signing transaction proposal");
|
||||
private static final ProgressTracker.Step COLLECTING_SIGNATURES = new ProgressTracker.Step(
|
||||
"Collecting signatures from other parties");
|
||||
private static final ProgressTracker.Step RECORDING = new ProgressTracker.Step(
|
||||
"Recording completed transaction");
|
||||
// DOCEND 1
|
||||
|
||||
// DOCSTART 2
|
||||
private static final ProgressTracker.Step VERIFYING_AND_SIGNING = new ProgressTracker.Step("Verifying and signing transaction proposal") {
|
||||
@Nullable
|
||||
@Override
|
||||
public ProgressTracker childProgressTracker() {
|
||||
return SignTransactionFlow.Companion.tracker();
|
||||
}
|
||||
};
|
||||
// DOCEND 2
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import com.template.TemplateContract;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import net.corda.core.contracts.Command;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
|
||||
// Replace Initiator's definition with:
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
public class IOUFlow extends FlowLogic<Void> {
|
||||
private final Integer iouValue;
|
||||
private final Party otherParty;
|
||||
|
||||
/**
|
||||
* The progress tracker provides checkpoints indicating the progress of the flow to observers.
|
||||
*/
|
||||
private final ProgressTracker progressTracker = new ProgressTracker();
|
||||
|
||||
public IOUFlow(Integer iouValue, Party otherParty) {
|
||||
this.iouValue = iouValue;
|
||||
this.otherParty = otherParty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressTracker getProgressTracker() {
|
||||
return progressTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flow logic is encapsulated within the call() method.
|
||||
*/
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// We retrieve the notary identity from the network map.
|
||||
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
||||
|
||||
// We create the transaction components.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
Command command = new Command<>(new TemplateContract.Commands.Action(), getOurIdentity().getOwningKey());
|
||||
|
||||
// We create a transaction builder and add the components.
|
||||
TransactionBuilder txBuilder = new TransactionBuilder(notary)
|
||||
.addOutputState(outputState, TemplateContract.ID)
|
||||
.addCommand(command);
|
||||
|
||||
// Signing the transaction.
|
||||
SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
|
||||
// Creating a session with the other party.
|
||||
FlowSession otherPartySession = initiateFlow(otherParty);
|
||||
|
||||
// We finalise the transaction and then send it to the counterparty.
|
||||
subFlow(new FinalityFlow(signedTx, otherPartySession));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -1,25 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.flows.*;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// DOCSTART 01
|
||||
// Replace Responder's definition with:
|
||||
@InitiatedBy(IOUFlow.class)
|
||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
private final FlowSession otherPartySession;
|
||||
|
||||
public IOUFlowResponder(FlowSession otherPartySession) {
|
||||
this.otherPartySession = otherPartySession;
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
subFlow(new ReceiveFinalityFlow(otherPartySession));
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -1,41 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.helloworld;
|
||||
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
// DOCSTART 01
|
||||
// Add this import:
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
// Replace TemplateState's definition with:
|
||||
public class IOUState implements ContractState {
|
||||
private final int value;
|
||||
private final Party lender;
|
||||
private final Party borrower;
|
||||
|
||||
public IOUState(int value, Party lender, Party borrower) {
|
||||
this.value = value;
|
||||
this.lender = lender;
|
||||
this.borrower = borrower;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Party getLender() {
|
||||
return lender;
|
||||
}
|
||||
|
||||
public Party getBorrower() {
|
||||
return borrower;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return Arrays.asList(lender, borrower);
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -1,55 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
import net.corda.core.contracts.CommandData;
|
||||
import net.corda.core.contracts.Contract;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import net.corda.core.contracts.CommandWithParties;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
|
||||
// Replace TemplateContract's definition with:
|
||||
public class IOUContract implements Contract {
|
||||
public static final String ID = "com.template.IOUContract";
|
||||
|
||||
// Our Create command.
|
||||
public static class Create implements CommandData {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(LedgerTransaction tx) {
|
||||
final CommandWithParties<IOUContract.Create> command = requireSingleCommand(tx.getCommands(), IOUContract.Create.class);
|
||||
|
||||
// Constraints on the shape of the transaction.
|
||||
if (!tx.getInputs().isEmpty())
|
||||
throw new IllegalArgumentException("No inputs should be consumed when issuing an IOU.");
|
||||
if (!(tx.getOutputs().size() == 1))
|
||||
throw new IllegalArgumentException("There should be one output state of type IOUState.");
|
||||
|
||||
// IOU-specific constraints.
|
||||
final IOUState output = tx.outputsOfType(IOUState.class).get(0);
|
||||
final Party lender = output.getLender();
|
||||
final Party borrower = output.getBorrower();
|
||||
if (output.getValue() <= 0)
|
||||
throw new IllegalArgumentException("The IOU's value must be non-negative.");
|
||||
if (lender.equals(borrower))
|
||||
throw new IllegalArgumentException("The lender and the borrower cannot be the same entity.");
|
||||
|
||||
// Constraints on the signers.
|
||||
final List<PublicKey> requiredSigners = command.getSigners();
|
||||
final List<PublicKey> expectedSigners = Arrays.asList(borrower.getOwningKey(), lender.getOwningKey());
|
||||
if (requiredSigners.size() != 2)
|
||||
throw new IllegalArgumentException("There must be two signers.");
|
||||
if (!(requiredSigners.containsAll(expectedSigners)))
|
||||
throw new IllegalArgumentException("The borrower and lender must be signers.");
|
||||
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -1,77 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.contracts.Command;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
import net.corda.core.utilities.ProgressTracker;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
// DOCEND 01
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
public class IOUFlow extends FlowLogic<Void> {
|
||||
private final Integer iouValue;
|
||||
private final Party otherParty;
|
||||
|
||||
/**
|
||||
* The progress tracker provides checkpoints indicating the progress of the flow to observers.
|
||||
*/
|
||||
private final ProgressTracker progressTracker = new ProgressTracker();
|
||||
|
||||
public IOUFlow(Integer iouValue, Party otherParty) {
|
||||
this.iouValue = iouValue;
|
||||
this.otherParty = otherParty;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressTracker getProgressTracker() {
|
||||
return progressTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* The flow logic is encapsulated within the call() method.
|
||||
*/
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
// DOCSTART 02
|
||||
// We retrieve the notary identity from the network map.
|
||||
Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
|
||||
|
||||
// We create the transaction components.
|
||||
IOUState outputState = new IOUState(iouValue, getOurIdentity(), otherParty);
|
||||
List<PublicKey> requiredSigners = Arrays.asList(getOurIdentity().getOwningKey(), otherParty.getOwningKey());
|
||||
Command command = new Command<>(new IOUContract.Create(), requiredSigners);
|
||||
|
||||
// We create a transaction builder and add the components.
|
||||
TransactionBuilder txBuilder = new TransactionBuilder(notary)
|
||||
.addOutputState(outputState, IOUContract.ID)
|
||||
.addCommand(command);
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(getServiceHub());
|
||||
|
||||
// Signing the transaction.
|
||||
SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
|
||||
|
||||
// Creating a session with the other party.
|
||||
FlowSession otherPartySession = initiateFlow(otherParty);
|
||||
|
||||
// Obtaining the counterparty's signature.
|
||||
SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
|
||||
signedTx, Arrays.asList(otherPartySession), CollectSignaturesFlow.tracker()));
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(new FinalityFlow(fullySignedTx, otherPartySession));
|
||||
|
||||
return null;
|
||||
// DOCEND 02
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable;
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
import net.corda.core.flows.*;
|
||||
import net.corda.core.transactions.SignedTransaction;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireThat;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// Define IOUFlowResponder:
|
||||
@InitiatedBy(IOUFlow.class)
|
||||
public class IOUFlowResponder extends FlowLogic<Void> {
|
||||
private final FlowSession otherPartySession;
|
||||
|
||||
public IOUFlowResponder(FlowSession otherPartySession) {
|
||||
this.otherPartySession = otherPartySession;
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
@Suspendable
|
||||
@Override
|
||||
public Void call() throws FlowException {
|
||||
class SignTxFlow extends SignTransactionFlow {
|
||||
private SignTxFlow(FlowSession otherPartySession) {
|
||||
super(otherPartySession);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkTransaction(SignedTransaction stx) {
|
||||
requireThat(require -> {
|
||||
ContractState output = stx.getTx().getOutputs().get(0).getData();
|
||||
require.using("This must be an IOU transaction.", output instanceof IOUState);
|
||||
IOUState iou = (IOUState) output;
|
||||
require.using("The IOU's value can't be too high.", iou.getValue() < 100);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SecureHash expectedTxId = subFlow(new SignTxFlow(otherPartySession)).getId();
|
||||
|
||||
subFlow(new ReceiveFinalityFlow(otherPartySession, expectedTxId));
|
||||
|
||||
return null;
|
||||
}
|
||||
// DOCEND 1
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.twoparty;
|
||||
|
||||
import net.corda.core.contracts.ContractState;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.Party;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class IOUState implements ContractState {
|
||||
private final int value;
|
||||
private final Party lender;
|
||||
private final Party borrower;
|
||||
|
||||
public IOUState(int value, Party lender, Party borrower) {
|
||||
this.value = value;
|
||||
this.lender = lender;
|
||||
this.borrower = borrower;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Party getLender() {
|
||||
return lender;
|
||||
}
|
||||
|
||||
public Party getBorrower() {
|
||||
return borrower;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AbstractParty> getParticipants() {
|
||||
return Arrays.asList(lender, borrower);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
// START 1
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.utilities.NetworkHostAndPort.Companion.parse
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.slf4j.Logger
|
||||
|
||||
class ClientRpcExample {
|
||||
companion object {
|
||||
val logger: Logger = loggerFor<ClientRpcExample>()
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
require(args.size == 3) { "Usage: TemplateClient <node address> <username> <password>" }
|
||||
val nodeAddress = parse(args[0])
|
||||
val username = args[1]
|
||||
val password = args[2]
|
||||
|
||||
val client = CordaRPCClient(nodeAddress)
|
||||
val connection = client.start(username, password)
|
||||
val cordaRPCOperations = connection.proxy
|
||||
|
||||
logger.info(cordaRPCOperations.currentNodeTime().toString())
|
||||
|
||||
connection.notifyServerAndClose()
|
||||
}
|
||||
}
|
||||
// END 1
|
@ -1,150 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.messaging.vaultQueryBy
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashExitFlow
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.internal.FINANCE_CORDAPPS
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Node
|
||||
import org.graphstream.graph.implementations.MultiGraph
|
||||
import rx.Observable
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* This is example code for the Client RPC API tutorial. The START/END comments are important and used by the documentation!
|
||||
*/
|
||||
|
||||
// START 1
|
||||
enum class PrintOrVisualise {
|
||||
Print,
|
||||
Visualise
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun main(args: Array<String>) {
|
||||
require(args.isNotEmpty()) { "Usage: <binary> [Print|Visualise]" }
|
||||
val printOrVisualise = PrintOrVisualise.valueOf(args[0])
|
||||
|
||||
val baseDirectory = Paths.get("build/rpc-api-tutorial")
|
||||
val user = User("user", "password", permissions = setOf(startFlow<CashIssueFlow>(),
|
||||
startFlow<CashPaymentFlow>(),
|
||||
startFlow<CashExitFlow>(),
|
||||
invokeRpc(CordaRPCOps::nodeInfo)
|
||||
))
|
||||
driver(DriverParameters(driverDirectory = baseDirectory, cordappsForAllNodes = FINANCE_CORDAPPS, waitForAllNodesToFinish = true)) {
|
||||
val node = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).get()
|
||||
// END 1
|
||||
|
||||
// START 2
|
||||
val client = CordaRPCClient(node.rpcAddress)
|
||||
val proxy = client.start("user", "password").proxy
|
||||
|
||||
thread {
|
||||
generateTransactions(proxy)
|
||||
}
|
||||
// END 2
|
||||
|
||||
// START 3
|
||||
val (transactions: List<SignedTransaction>, futureTransactions: Observable<SignedTransaction>) = proxy.internalVerifiedTransactionsFeed()
|
||||
// END 3
|
||||
|
||||
// START 4
|
||||
when (printOrVisualise) {
|
||||
PrintOrVisualise.Print -> {
|
||||
futureTransactions.startWith(transactions).subscribe { transaction ->
|
||||
println("NODE ${transaction.id}")
|
||||
transaction.tx.inputs.forEach { (txhash) ->
|
||||
println("EDGE $txhash ${transaction.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
// END 4
|
||||
// START 5
|
||||
PrintOrVisualise.Visualise -> {
|
||||
val graph = MultiGraph("transactions")
|
||||
transactions.forEach { transaction ->
|
||||
graph.addNode<Node>("${transaction.id}")
|
||||
}
|
||||
transactions.forEach { transaction ->
|
||||
transaction.tx.inputs.forEach { ref ->
|
||||
graph.addEdge<Edge>("$ref", "${ref.txhash}", "${transaction.id}")
|
||||
}
|
||||
}
|
||||
futureTransactions.subscribe { transaction ->
|
||||
graph.addNode<Node>("${transaction.id}")
|
||||
transaction.tx.inputs.forEach { ref ->
|
||||
graph.addEdge<Edge>("$ref", "${ref.txhash}", "${transaction.id}")
|
||||
}
|
||||
}
|
||||
graph.display()
|
||||
}
|
||||
}
|
||||
// END 5
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
// START 6
|
||||
fun generateTransactions(proxy: CordaRPCOps) {
|
||||
val vault = proxy.vaultQueryBy<Cash.State>().states
|
||||
|
||||
var ownedQuantity = vault.fold(0L) { sum, state ->
|
||||
sum + state.state.data.amount.quantity
|
||||
}
|
||||
val issueRef = OpaqueBytes.of(0)
|
||||
val notary = proxy.notaryIdentities().first()
|
||||
val me = proxy.nodeInfo().legalIdentities.first()
|
||||
while (true) {
|
||||
Thread.sleep(1000)
|
||||
val random = SplittableRandom()
|
||||
val n = random.nextDouble()
|
||||
if (ownedQuantity > 10000 && n > 0.8) {
|
||||
val quantity = Math.abs(random.nextLong()) % 2000
|
||||
proxy.startFlow(::CashExitFlow, Amount(quantity, USD), issueRef)
|
||||
ownedQuantity -= quantity
|
||||
} else if (ownedQuantity > 1000 && n < 0.7) {
|
||||
val quantity = Math.abs(random.nextLong() % Math.min(ownedQuantity, 2000))
|
||||
proxy.startFlow(::CashPaymentFlow, Amount(quantity, USD), me)
|
||||
} else {
|
||||
val quantity = Math.abs(random.nextLong() % 1000)
|
||||
proxy.startFlow(::CashIssueFlow, Amount(quantity, USD), issueRef, notary)
|
||||
ownedQuantity += quantity
|
||||
}
|
||||
}
|
||||
}
|
||||
// END 6
|
||||
|
||||
// START 7
|
||||
// Not annotated, so need to whitelist manually.
|
||||
data class ExampleRPCValue(val foo: String)
|
||||
|
||||
// Annotated, so no need to whitelist manually.
|
||||
@CordaSerializable
|
||||
data class ExampleRPCValue2(val bar: Int)
|
||||
|
||||
class ExampleRPCSerializationWhitelist : SerializationWhitelist {
|
||||
// Add classes like this.
|
||||
override val whitelist = listOf(ExampleRPCValue::class.java)
|
||||
}
|
||||
// END 7
|
@ -1,93 +0,0 @@
|
||||
@file:Suppress("DEPRECATION", "unused", "UNUSED_PARAMETER")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
|
||||
private fun dummyTransactionWithParticipant(party: Party): SignedTransaction = TODO()
|
||||
|
||||
// DOCSTART SimpleFlowUsingOldApi
|
||||
class SimpleFlowUsingOldApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx = dummyTransactionWithParticipant(counterparty)
|
||||
return subFlow(FinalityFlow(stx))
|
||||
}
|
||||
}
|
||||
// DOCEND SimpleFlowUsingOldApi
|
||||
|
||||
// DOCSTART SimpleFlowUsingNewApi
|
||||
// Notice how the flow *must* now be an initiating flow even when it wasn't before.
|
||||
@InitiatingFlow
|
||||
class SimpleFlowUsingNewApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx = dummyTransactionWithParticipant(counterparty)
|
||||
// For each non-local participant in the transaction we must initiate a flow session with them.
|
||||
val session = initiateFlow(counterparty)
|
||||
return subFlow(FinalityFlow(stx, session))
|
||||
}
|
||||
}
|
||||
// DOCEND SimpleFlowUsingNewApi
|
||||
|
||||
// DOCSTART SimpleNewResponderFlow
|
||||
// All participants will run this flow to receive and record the finalised transaction into their vault.
|
||||
@InitiatedBy(SimpleFlowUsingNewApi::class)
|
||||
class SimpleNewResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
// DOCEND SimpleNewResponderFlow
|
||||
|
||||
// DOCSTART ExistingInitiatingFlow
|
||||
// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
|
||||
// to allow for backwards compatibility with nodes running the old CorDapp.
|
||||
@InitiatingFlow(version = 2)
|
||||
class ExistingInitiatingFlow(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val partiallySignedTx = dummyTransactionWithParticipant(counterparty)
|
||||
val session = initiateFlow(counterparty)
|
||||
val fullySignedTx = subFlow(CollectSignaturesFlow(partiallySignedTx, listOf(session)))
|
||||
// Determine which version of the flow that other side is using.
|
||||
return if (session.getCounterpartyFlowInfo().flowVersion == 1) {
|
||||
// Use the old API if the other side is using the previous version of the flow.
|
||||
subFlow(FinalityFlow(fullySignedTx))
|
||||
} else {
|
||||
// Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
|
||||
subFlow(FinalityFlow(fullySignedTx, session))
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND ExistingInitiatingFlow
|
||||
|
||||
@InitiatedBy(ExistingInitiatingFlow::class)
|
||||
class ExistingResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// DOCSTART ExistingResponderFlow
|
||||
// First we have to run the SignTransactionFlow, which will return a SignedTransaction.
|
||||
val txWeJustSigned = subFlow(object : SignTransactionFlow(otherSide) {
|
||||
@Suspendable
|
||||
override fun checkTransaction(stx: SignedTransaction) {
|
||||
// Implement responder flow transaction checks here
|
||||
}
|
||||
})
|
||||
|
||||
if (otherSide.getCounterpartyFlowInfo().flowVersion >= 2) {
|
||||
// The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
|
||||
// If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
|
||||
// that was just signed.
|
||||
subFlow(ReceiveFinalityFlow(otherSide, expectedTxId = txWeJustSigned.id))
|
||||
} else {
|
||||
// Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
|
||||
// will automatically record the finalised transaction using the old insecure mechanism.
|
||||
}
|
||||
// DOCEND ExistingResponderFlow
|
||||
}
|
||||
}
|
@ -1,664 +0,0 @@
|
||||
@file:Suppress("UNUSED_VARIABLE", "unused", "DEPRECATION")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.node.services.Vault.Page
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.ProgressTracker.Step
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import java.security.PublicKey
|
||||
import java.security.Signature
|
||||
import java.time.Instant
|
||||
|
||||
// ``InitiatorFlow`` is our first flow, and will communicate with
|
||||
// ``ResponderFlow``, below.
|
||||
// We mark ``InitiatorFlow`` as an ``InitiatingFlow``, allowing it to be
|
||||
// started directly by the node.
|
||||
@InitiatingFlow
|
||||
// We also mark ``InitiatorFlow`` as ``StartableByRPC``, allowing the
|
||||
// node's owner to start the flow via RPC.
|
||||
@StartableByRPC
|
||||
// Every flow must subclass ``FlowLogic``. The generic indicates the
|
||||
// flow's return type.
|
||||
class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: Party, val regulator: Party) : FlowLogic<Unit>() {
|
||||
|
||||
/**---------------------------------
|
||||
* WIRING UP THE PROGRESS TRACKER *
|
||||
---------------------------------**/
|
||||
// Giving our flow a progress tracker allows us to see the flow's
|
||||
// progress visually in our node's CRaSH shell.
|
||||
// DOCSTART 17
|
||||
companion object {
|
||||
object ID_OTHER_NODES : Step("Identifying other nodes on the network.")
|
||||
object SENDING_AND_RECEIVING_DATA : Step("Sending data between parties.")
|
||||
object EXTRACTING_VAULT_STATES : Step("Extracting states from the vault.")
|
||||
object OTHER_TX_COMPONENTS : Step("Gathering a transaction's other components.")
|
||||
object TX_BUILDING : Step("Building a transaction.")
|
||||
object TX_SIGNING : Step("Signing a transaction.")
|
||||
object TX_VERIFICATION : Step("Verifying a transaction.")
|
||||
object SIGS_GATHERING : Step("Gathering a transaction's signatures.") {
|
||||
// Wiring up a child progress tracker allows us to see the
|
||||
// subflow's progress steps in our flow's progress tracker.
|
||||
override fun childProgressTracker() = CollectSignaturesFlow.tracker()
|
||||
}
|
||||
|
||||
object VERIFYING_SIGS : Step("Verifying a transaction's signatures.")
|
||||
object FINALISATION : Step("Finalising a transaction.") {
|
||||
override fun childProgressTracker() = FinalityFlow.tracker()
|
||||
}
|
||||
|
||||
fun tracker() = ProgressTracker(
|
||||
ID_OTHER_NODES,
|
||||
SENDING_AND_RECEIVING_DATA,
|
||||
EXTRACTING_VAULT_STATES,
|
||||
OTHER_TX_COMPONENTS,
|
||||
TX_BUILDING,
|
||||
TX_SIGNING,
|
||||
TX_VERIFICATION,
|
||||
SIGS_GATHERING,
|
||||
VERIFYING_SIGS,
|
||||
FINALISATION
|
||||
)
|
||||
}
|
||||
// DOCEND 17
|
||||
|
||||
override val progressTracker: ProgressTracker = tracker()
|
||||
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// We'll be using a dummy public key for demonstration purposes.
|
||||
val dummyPubKey: PublicKey = generateKeyPair().public
|
||||
|
||||
/**--------------------------
|
||||
* IDENTIFYING OTHER NODES *
|
||||
--------------------------**/
|
||||
// DOCSTART 18
|
||||
progressTracker.currentStep = ID_OTHER_NODES
|
||||
// DOCEND 18
|
||||
|
||||
// Every transaction needs a notary:
|
||||
// - To prevent double-spends if the transaction has inputs
|
||||
// - To serve as a timestamping authority if the transaction has a
|
||||
// time-window
|
||||
// We retrieve the notary from the network map.
|
||||
// DOCSTART 01
|
||||
val notaryName: CordaX500Name = CordaX500Name(
|
||||
organisation = "Notary Service",
|
||||
locality = "London",
|
||||
country = "GB")
|
||||
val notary: Party = serviceHub.networkMapCache.getNotary(notaryName)!!
|
||||
// DOCEND 01
|
||||
|
||||
// We may also need to identify a specific counterparty. We do so
|
||||
// using the identity service.
|
||||
// DOCSTART 02
|
||||
val counterpartyName: CordaX500Name = CordaX500Name(
|
||||
organisation = "NodeA",
|
||||
locality = "London",
|
||||
country = "GB")
|
||||
val namedCounterparty: Party = serviceHub.identityService.wellKnownPartyFromX500Name(counterpartyName) ?:
|
||||
throw IllegalArgumentException("Couldn't find counterparty for NodeA in identity service")
|
||||
val keyedCounterparty: Party = serviceHub.identityService.partyFromKey(dummyPubKey) ?:
|
||||
throw IllegalArgumentException("Couldn't find counterparty with key: $dummyPubKey in identity service")
|
||||
// DOCEND 02
|
||||
|
||||
/**-----------------------------
|
||||
* SENDING AND RECEIVING DATA *
|
||||
-----------------------------**/
|
||||
progressTracker.currentStep = SENDING_AND_RECEIVING_DATA
|
||||
|
||||
// We start by initiating a flow session with the counterparty. We
|
||||
// will use this session to send and receive messages from the
|
||||
// counterparty.
|
||||
// DOCSTART initiateFlow
|
||||
val counterpartySession: FlowSession = initiateFlow(counterparty)
|
||||
// DOCEND initiateFlow
|
||||
|
||||
// We can send arbitrary data to a counterparty.
|
||||
// If this is the first ``send``, the counterparty will either:
|
||||
// 1. Ignore the message if they are not registered to respond
|
||||
// to messages from this flow.
|
||||
// 2. Start the flow they have registered to respond to this flow,
|
||||
// and run the flow until the first call to ``receive``, at
|
||||
// which point they process the message.
|
||||
// In other words, we are assuming that the counterparty is
|
||||
// registered to respond to this flow, and has a corresponding
|
||||
// ``receive`` call.
|
||||
// DOCSTART 04
|
||||
counterpartySession.send(Any())
|
||||
// DOCEND 04
|
||||
|
||||
// We can wait to receive arbitrary data of a specific type from a
|
||||
// counterparty. Again, this implies a corresponding ``send`` call
|
||||
// in the counterparty's flow. A few scenarios:
|
||||
// - We never receive a message back. In the current design, the
|
||||
// flow is paused until the node's owner kills the flow.
|
||||
// - Instead of sending a message back, the counterparty throws a
|
||||
// ``FlowException``. This exception is propagated back to us,
|
||||
// and we can use the error message to establish what happened.
|
||||
// - We receive a message back, but it's of the wrong type. In
|
||||
// this case, a ``FlowException`` is thrown.
|
||||
// - We receive back a message of the correct type. All is good.
|
||||
//
|
||||
// Upon calling ``receive()`` (or ``sendAndReceive()``), the
|
||||
// ``FlowLogic`` is suspended until it receives a response.
|
||||
//
|
||||
// We receive the data wrapped in an ``UntrustworthyData``
|
||||
// instance. This is a reminder that the data we receive may not
|
||||
// be what it appears to be! We must unwrap the
|
||||
// ``UntrustworthyData`` using a lambda.
|
||||
// DOCSTART 05
|
||||
val packet1: UntrustworthyData<Int> = counterpartySession.receive<Int>()
|
||||
val int: Int = packet1.unwrap { data ->
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
// Return the object.
|
||||
data
|
||||
}
|
||||
// DOCEND 05
|
||||
|
||||
// We can also use a single call to send data to a counterparty
|
||||
// and wait to receive data of a specific type back. The type of
|
||||
// data sent doesn't need to match the type of the data received
|
||||
// back.
|
||||
// DOCSTART 07
|
||||
val packet2: UntrustworthyData<Boolean> = counterpartySession.sendAndReceive<Boolean>("You can send and receive any class!")
|
||||
val boolean: Boolean = packet2.unwrap { data ->
|
||||
// Perform checking on the object received.
|
||||
// T O D O: Check the received object.
|
||||
// Return the object.
|
||||
data
|
||||
}
|
||||
// DOCEND 07
|
||||
|
||||
// We're not limited to sending to and receiving from a single
|
||||
// counterparty. A flow can send messages to as many parties as it
|
||||
// likes, and each party can invoke a different response flow.
|
||||
// DOCSTART 06
|
||||
val regulatorSession: FlowSession = initiateFlow(regulator)
|
||||
regulatorSession.send(Any())
|
||||
val packet3: UntrustworthyData<Any> = regulatorSession.receive<Any>()
|
||||
// DOCEND 06
|
||||
|
||||
// We may also batch receives in order to increase performance. This
|
||||
// ensures that only a single checkpoint is created for all received
|
||||
// messages.
|
||||
// Type-safe variant:
|
||||
val signatures: List<UntrustworthyData<Signature>> =
|
||||
receiveAll(Signature::class.java, listOf(counterpartySession, regulatorSession))
|
||||
// Dynamic variant:
|
||||
val messages: Map<FlowSession, UntrustworthyData<*>> =
|
||||
receiveAllMap(mapOf(
|
||||
counterpartySession to Boolean::class.java,
|
||||
regulatorSession to String::class.java
|
||||
))
|
||||
|
||||
/**-----------------------------------
|
||||
* EXTRACTING STATES FROM THE VAULT *
|
||||
-----------------------------------**/
|
||||
progressTracker.currentStep = EXTRACTING_VAULT_STATES
|
||||
|
||||
// Let's assume there are already some ``DummyState``s in our
|
||||
// node's vault, stored there as a result of running past flows,
|
||||
// and we want to consume them in a transaction. There are many
|
||||
// ways to extract these states from our vault.
|
||||
|
||||
// For example, we would extract any unconsumed ``DummyState``s
|
||||
// from our vault as follows:
|
||||
val criteria: VaultQueryCriteria = VaultQueryCriteria() // default is UNCONSUMED
|
||||
val results: Page<DummyState> = serviceHub.vaultService.queryBy<DummyState>(criteria)
|
||||
val dummyStates: List<StateAndRef<DummyState>> = results.states
|
||||
|
||||
// For a full list of the available ways of extracting states from
|
||||
// the vault, see the Vault Query docs page.
|
||||
|
||||
// When building a transaction, input states are passed in as
|
||||
// ``StateRef`` instances, which pair the hash of the transaction
|
||||
// that generated the state with the state's index in the outputs
|
||||
// of that transaction. In practice, we'd pass the transaction hash
|
||||
// or the ``StateRef`` as a parameter to the flow, or extract the
|
||||
// ``StateRef`` from our vault.
|
||||
// DOCSTART 20
|
||||
val ourStateRef: StateRef = StateRef(SecureHash.sha256("DummyTransactionHash"), 0)
|
||||
// DOCEND 20
|
||||
// A ``StateAndRef`` pairs a ``StateRef`` with the state it points to.
|
||||
// DOCSTART 21
|
||||
val ourStateAndRef: StateAndRef<DummyState> = serviceHub.toStateAndRef<DummyState>(ourStateRef)
|
||||
// DOCEND 21
|
||||
|
||||
/**-----------------------------------------
|
||||
* GATHERING OTHER TRANSACTION COMPONENTS *
|
||||
-----------------------------------------**/
|
||||
progressTracker.currentStep = OTHER_TX_COMPONENTS
|
||||
|
||||
// Reference input states are constructed from StateAndRefs.
|
||||
// DOCSTART 55
|
||||
val referenceState: ReferencedStateAndRef<DummyState> = ourStateAndRef.referenced()
|
||||
// DOCEND 55
|
||||
// Output states are constructed from scratch.
|
||||
// DOCSTART 22
|
||||
val ourOutputState: DummyState = DummyState()
|
||||
// DOCEND 22
|
||||
// Or as copies of other states with some properties changed.
|
||||
@Suppress("MagicNumber") // literally a magic number
|
||||
// DOCSTART 23
|
||||
val ourOtherOutputState: DummyState = ourOutputState.copy(magicNumber = 77)
|
||||
// DOCEND 23
|
||||
|
||||
// We then need to pair our output state with a contract.
|
||||
// DOCSTART 47
|
||||
val ourOutput: StateAndContract = StateAndContract(ourOutputState, DummyContract.PROGRAM_ID)
|
||||
// DOCEND 47
|
||||
|
||||
// Commands pair a ``CommandData`` instance with a list of
|
||||
// public keys. To be valid, the transaction requires a signature
|
||||
// matching every public key in all of the transaction's commands.
|
||||
// DOCSTART 24
|
||||
val commandData: DummyContract.Commands.Create = DummyContract.Commands.Create()
|
||||
val ourPubKey: PublicKey = serviceHub.myInfo.legalIdentitiesAndCerts.first().owningKey
|
||||
val counterpartyPubKey: PublicKey = counterparty.owningKey
|
||||
val requiredSigners: List<PublicKey> = listOf(ourPubKey, counterpartyPubKey)
|
||||
val ourCommand: Command<DummyContract.Commands.Create> = Command(commandData, requiredSigners)
|
||||
// DOCEND 24
|
||||
|
||||
// ``CommandData`` can either be:
|
||||
// 1. Of type ``TypeOnlyCommandData``, in which case it only
|
||||
// serves to attach signers to the transaction and possibly
|
||||
// fork the contract's verification logic.
|
||||
val typeOnlyCommandData: TypeOnlyCommandData = DummyContract.Commands.Create()
|
||||
// 2. Include additional data which can be used by the contract
|
||||
// during verification, alongside fulfilling the roles above.
|
||||
val commandDataWithData: CommandData = Cash.Commands.Issue()
|
||||
|
||||
// Attachments are identified by their hash.
|
||||
// The attachment with the corresponding hash must have been
|
||||
// uploaded ahead of time via the node's RPC interface.
|
||||
// DOCSTART 25
|
||||
val ourAttachment: SecureHash = SecureHash.sha256("DummyAttachment")
|
||||
// DOCEND 25
|
||||
|
||||
// Time windows can have a start and end time, or be open at either end.
|
||||
// DOCSTART 26
|
||||
val ourTimeWindow: TimeWindow = TimeWindow.between(Instant.MIN, Instant.MAX)
|
||||
val ourAfter: TimeWindow = TimeWindow.fromOnly(Instant.MIN)
|
||||
val ourBefore: TimeWindow = TimeWindow.untilOnly(Instant.MAX)
|
||||
// DOCEND 26
|
||||
|
||||
// We can also define a time window as an ``Instant`` +/- a time
|
||||
// tolerance (e.g. 30 seconds):
|
||||
// DOCSTART 42
|
||||
val ourTimeWindow2: TimeWindow = TimeWindow.withTolerance(serviceHub.clock.instant(), 30.seconds)
|
||||
// DOCEND 42
|
||||
// Or as a start-time plus a duration:
|
||||
// DOCSTART 43
|
||||
val ourTimeWindow3: TimeWindow = TimeWindow.fromStartAndDuration(serviceHub.clock.instant(), 30.seconds)
|
||||
// DOCEND 43
|
||||
|
||||
/**-----------------------
|
||||
* TRANSACTION BUILDING *
|
||||
-----------------------**/
|
||||
progressTracker.currentStep = TX_BUILDING
|
||||
|
||||
// If our transaction has input states or a time-window, we must instantiate it with a
|
||||
// notary.
|
||||
// DOCSTART 19
|
||||
val txBuilder: TransactionBuilder = TransactionBuilder(notary)
|
||||
// DOCEND 19
|
||||
|
||||
// Otherwise, we can choose to instantiate it without one:
|
||||
// DOCSTART 46
|
||||
val txBuilderNoNotary: TransactionBuilder = TransactionBuilder()
|
||||
// DOCEND 46
|
||||
|
||||
// We add items to the transaction builder using ``TransactionBuilder.withItems``:
|
||||
// DOCSTART 27
|
||||
txBuilder.withItems(
|
||||
// Inputs, as ``StateAndRef``s that reference the outputs of previous transactions
|
||||
ourStateAndRef,
|
||||
// Outputs, as ``StateAndContract``s
|
||||
ourOutput,
|
||||
// Commands, as ``Command``s
|
||||
ourCommand,
|
||||
// Attachments, as ``SecureHash``es
|
||||
ourAttachment,
|
||||
// A time-window, as ``TimeWindow``
|
||||
ourTimeWindow
|
||||
)
|
||||
// DOCEND 27
|
||||
|
||||
// We can also add items using methods for the individual components.
|
||||
|
||||
// The individual methods for adding input states and attachments:
|
||||
// DOCSTART 28
|
||||
txBuilder.addInputState(ourStateAndRef)
|
||||
txBuilder.addAttachment(ourAttachment)
|
||||
// DOCEND 28
|
||||
|
||||
// An output state can be added as a ``ContractState``, contract class name and notary.
|
||||
// DOCSTART 49
|
||||
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID, notary)
|
||||
// DOCEND 49
|
||||
// We can also leave the notary field blank, in which case the transaction's default
|
||||
// notary is used.
|
||||
// DOCSTART 50
|
||||
txBuilder.addOutputState(ourOutputState, DummyContract.PROGRAM_ID)
|
||||
// DOCEND 50
|
||||
// Or we can add the output state as a ``TransactionState``, which already specifies
|
||||
// the output's contract and notary.
|
||||
// DOCSTART 51
|
||||
val txState: TransactionState<DummyState> = TransactionState(ourOutputState, DummyContract.PROGRAM_ID, notary)
|
||||
// DOCEND 51
|
||||
|
||||
// Commands can be added as ``Command``s.
|
||||
// DOCSTART 52
|
||||
txBuilder.addCommand(ourCommand)
|
||||
// DOCEND 52
|
||||
// Or as ``CommandData`` and a ``vararg PublicKey``.
|
||||
// DOCSTART 53
|
||||
txBuilder.addCommand(commandData, ourPubKey, counterpartyPubKey)
|
||||
// DOCEND 53
|
||||
|
||||
// We can set a time-window directly.
|
||||
// DOCSTART 44
|
||||
txBuilder.setTimeWindow(ourTimeWindow)
|
||||
// DOCEND 44
|
||||
// Or as a start time plus a duration (e.g. 45 seconds).
|
||||
// DOCSTART 45
|
||||
txBuilder.setTimeWindow(serviceHub.clock.instant(), 45.seconds)
|
||||
// DOCEND 45
|
||||
|
||||
/**----------------------
|
||||
* TRANSACTION SIGNING *
|
||||
----------------------**/
|
||||
progressTracker.currentStep = TX_SIGNING
|
||||
|
||||
// We finalise the transaction by signing it, converting it into a
|
||||
// ``SignedTransaction``.
|
||||
// DOCSTART 29
|
||||
val onceSignedTx: SignedTransaction = serviceHub.signInitialTransaction(txBuilder)
|
||||
// DOCEND 29
|
||||
// We can also sign the transaction using a different public key:
|
||||
// DOCSTART 30
|
||||
val otherIdentity: PartyAndCertificate = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||
val onceSignedTx2: SignedTransaction = serviceHub.signInitialTransaction(txBuilder, otherIdentity.owningKey)
|
||||
// DOCEND 30
|
||||
|
||||
// If instead this was a ``SignedTransaction`` that we'd received
|
||||
// from a counterparty and we needed to sign it, we would add our
|
||||
// signature using:
|
||||
// DOCSTART 38
|
||||
val twiceSignedTx: SignedTransaction = serviceHub.addSignature(onceSignedTx)
|
||||
// DOCEND 38
|
||||
// Or, if we wanted to use a different public key:
|
||||
val otherIdentity2: PartyAndCertificate = serviceHub.keyManagementService.freshKeyAndCert(ourIdentityAndCert, false)
|
||||
// DOCSTART 39
|
||||
val twiceSignedTx2: SignedTransaction = serviceHub.addSignature(onceSignedTx, otherIdentity2.owningKey)
|
||||
// DOCEND 39
|
||||
|
||||
// We can also generate a signature over the transaction without
|
||||
// adding it to the transaction itself. We may do this when
|
||||
// sending just the signature in a flow instead of returning the
|
||||
// entire transaction with our signature. This way, the receiving
|
||||
// node does not need to check we haven't changed anything in the
|
||||
// transaction.
|
||||
// DOCSTART 40
|
||||
val sig: TransactionSignature = serviceHub.createSignature(onceSignedTx)
|
||||
// DOCEND 40
|
||||
// And again, if we wanted to use a different public key:
|
||||
// DOCSTART 41
|
||||
val sig2: TransactionSignature = serviceHub.createSignature(onceSignedTx, otherIdentity2.owningKey)
|
||||
// DOCEND 41
|
||||
|
||||
// In practice, however, the process of gathering every signature
|
||||
// but the first can be automated using ``CollectSignaturesFlow``.
|
||||
// See the "Gathering Signatures" section below.
|
||||
|
||||
/**---------------------------
|
||||
* TRANSACTION VERIFICATION *
|
||||
---------------------------**/
|
||||
progressTracker.currentStep = TX_VERIFICATION
|
||||
|
||||
// Verifying a transaction will also verify every transaction in
|
||||
// the transaction's dependency chain, which will require
|
||||
// transaction data access on counterparty's node. The
|
||||
// ``SendTransactionFlow`` can be used to automate the sending and
|
||||
// data vending process. The ``SendTransactionFlow`` will listen
|
||||
// for data request until the transaction is resolved and verified
|
||||
// on the other side:
|
||||
// DOCSTART 12
|
||||
subFlow(SendTransactionFlow(counterpartySession, twiceSignedTx))
|
||||
|
||||
// Optional request verification to further restrict data access.
|
||||
subFlow(object : SendTransactionFlow(counterpartySession, twiceSignedTx) {
|
||||
override fun verifyDataRequest(dataRequest: FetchDataFlow.Request.Data) {
|
||||
// Extra request verification.
|
||||
}
|
||||
})
|
||||
// DOCEND 12
|
||||
|
||||
// We can receive the transaction using ``ReceiveTransactionFlow``,
|
||||
// which will automatically download all the dependencies and verify
|
||||
// the transaction
|
||||
// DOCSTART 13
|
||||
val verifiedTransaction = subFlow(ReceiveTransactionFlow(counterpartySession))
|
||||
// DOCEND 13
|
||||
|
||||
// We can also send and receive a `StateAndRef` dependency chain
|
||||
// and automatically resolve its dependencies.
|
||||
// DOCSTART 14
|
||||
subFlow(SendStateAndRefFlow(counterpartySession, dummyStates))
|
||||
|
||||
// On the receive side ...
|
||||
val resolvedStateAndRef = subFlow(ReceiveStateAndRefFlow<DummyState>(counterpartySession))
|
||||
// DOCEND 14
|
||||
|
||||
// We can now verify the transaction to ensure that it satisfies
|
||||
// the contracts of all the transaction's input and output states.
|
||||
// DOCSTART 33
|
||||
twiceSignedTx.verify(serviceHub)
|
||||
// DOCEND 33
|
||||
|
||||
// We'll often want to perform our own additional verification
|
||||
// too. Just because a transaction is valid based on the contract
|
||||
// rules and requires our signature doesn't mean we have to
|
||||
// sign it! We need to make sure the transaction represents an
|
||||
// agreement we actually want to enter into.
|
||||
|
||||
// To do this, we need to convert our ``SignedTransaction``
|
||||
// into a ``LedgerTransaction``. This will use our ServiceHub
|
||||
// to resolve the transaction's inputs and attachments into
|
||||
// actual objects, rather than just references.
|
||||
// DOCSTART 32
|
||||
val ledgerTx: LedgerTransaction = twiceSignedTx.toLedgerTransaction(serviceHub)
|
||||
// DOCEND 32
|
||||
|
||||
// We can now perform our additional verification.
|
||||
// DOCSTART 34
|
||||
val outputState: DummyState = ledgerTx.outputsOfType<DummyState>().single()
|
||||
if (outputState.magicNumber == 777) {
|
||||
// ``FlowException`` is a special exception type. It will be
|
||||
// propagated back to any counterparty flows waiting for a
|
||||
// message from this flow, notifying them that the flow has
|
||||
// failed.
|
||||
throw FlowException("We expected a magic number of 777.")
|
||||
}
|
||||
// DOCEND 34
|
||||
|
||||
// Of course, if you are not a required signer on the transaction,
|
||||
// you have no power to decide whether it is valid or not. If it
|
||||
// requires signatures from all the required signers and is
|
||||
// contractually valid, it's a valid ledger update.
|
||||
|
||||
/**-----------------------
|
||||
* GATHERING SIGNATURES *
|
||||
-----------------------**/
|
||||
progressTracker.currentStep = SIGS_GATHERING
|
||||
|
||||
// The list of parties who need to sign a transaction is dictated
|
||||
// by the transaction's commands. Once we've signed a transaction
|
||||
// ourselves, we can automatically gather the signatures of the
|
||||
// other required signers using ``CollectSignaturesFlow``.
|
||||
// The responder flow will need to call ``SignTransactionFlow``.
|
||||
// DOCSTART 15
|
||||
val fullySignedTx: SignedTransaction = subFlow(CollectSignaturesFlow(twiceSignedTx, setOf(counterpartySession, regulatorSession), SIGS_GATHERING.childProgressTracker()))
|
||||
// DOCEND 15
|
||||
|
||||
/**-----------------------
|
||||
* VERIFYING SIGNATURES *
|
||||
-----------------------**/
|
||||
progressTracker.currentStep = VERIFYING_SIGS
|
||||
|
||||
// We can verify that a transaction has all the required
|
||||
// signatures, and that they're all valid, by running:
|
||||
// DOCSTART 35
|
||||
fullySignedTx.verifyRequiredSignatures()
|
||||
// DOCEND 35
|
||||
|
||||
// If the transaction is only partially signed, we have to pass in
|
||||
// a vararg of the public keys corresponding to the missing
|
||||
// signatures, explicitly telling the system not to check them.
|
||||
// DOCSTART 36
|
||||
onceSignedTx.verifySignaturesExcept(counterpartyPubKey)
|
||||
// DOCEND 36
|
||||
|
||||
// There is also an overload of ``verifySignaturesExcept`` which accepts
|
||||
// a ``Collection`` of the public keys corresponding to the missing
|
||||
// signatures.
|
||||
// DOCSTART 54
|
||||
onceSignedTx.verifySignaturesExcept(listOf(counterpartyPubKey))
|
||||
// DOCEND 54
|
||||
|
||||
// We can also choose to only check the signatures that are
|
||||
// present. BE VERY CAREFUL - this function provides no guarantees
|
||||
// that the signatures are correct, or that none are missing.
|
||||
// DOCSTART 37
|
||||
twiceSignedTx.checkSignaturesAreValid()
|
||||
// DOCEND 37
|
||||
|
||||
/**-----------------------------
|
||||
* FINALISING THE TRANSACTION *
|
||||
-----------------------------**/
|
||||
progressTracker.currentStep = FINALISATION
|
||||
|
||||
// We notarise the transaction and get it recorded in the vault of
|
||||
// the participants of all the transaction's states.
|
||||
// DOCSTART 09
|
||||
val notarisedTx1: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, listOf(counterpartySession), FINALISATION.childProgressTracker()))
|
||||
// DOCEND 09
|
||||
// We can also choose to send it to additional parties who aren't one
|
||||
// of the state's participants.
|
||||
// DOCSTART 10
|
||||
val partySessions: List<FlowSession> = listOf(counterpartySession, initiateFlow(regulator))
|
||||
val notarisedTx2: SignedTransaction = subFlow(FinalityFlow(fullySignedTx, partySessions, FINALISATION.childProgressTracker()))
|
||||
// DOCEND 10
|
||||
|
||||
// DOCSTART FlowSession porting
|
||||
send(regulator, Any()) // Old API
|
||||
// becomes
|
||||
val session = initiateFlow(regulator)
|
||||
session.send(Any())
|
||||
// DOCEND FlowSession porting
|
||||
}
|
||||
}
|
||||
|
||||
// ``ResponderFlow`` is our second flow, and will communicate with
|
||||
// ``InitiatorFlow``.
|
||||
// We mark ``ResponderFlow`` as an ``InitiatedByFlow``, meaning that it
|
||||
// can only be started in response to a message from its initiating flow.
|
||||
// That's ``InitiatorFlow`` in this case.
|
||||
// Each node also has several flow pairs registered by default - see
|
||||
// ``AbstractNode.installCoreFlows``.
|
||||
@InitiatedBy(InitiatorFlow::class)
|
||||
class ResponderFlow(val counterpartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING_AND_SENDING_DATA : Step("Sending data between parties.")
|
||||
object SIGNING : Step("Responding to CollectSignaturesFlow.")
|
||||
object FINALISATION : Step("Finalising a transaction.")
|
||||
|
||||
fun tracker() = ProgressTracker(
|
||||
RECEIVING_AND_SENDING_DATA,
|
||||
SIGNING,
|
||||
FINALISATION
|
||||
)
|
||||
}
|
||||
|
||||
override val progressTracker: ProgressTracker = tracker()
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// The ``ResponderFlow` has all the same APIs available. It looks
|
||||
// up network information, sends and receives data, and constructs
|
||||
// transactions in exactly the same way.
|
||||
|
||||
/**-----------------------------
|
||||
* SENDING AND RECEIVING DATA *
|
||||
-----------------------------**/
|
||||
progressTracker.currentStep = RECEIVING_AND_SENDING_DATA
|
||||
|
||||
// We need to respond to the messages sent by the initiator:
|
||||
// 1. They sent us an ``Any`` instance
|
||||
// 2. They waited to receive an ``Integer`` instance back
|
||||
// 3. They sent a ``String`` instance and waited to receive a
|
||||
// ``Boolean`` instance back
|
||||
// Our side of the flow must mirror these calls.
|
||||
// DOCSTART 08
|
||||
val any: Any = counterpartySession.receive<Any>().unwrap { data -> data }
|
||||
val string: String = counterpartySession.sendAndReceive<String>(99).unwrap { data -> data }
|
||||
counterpartySession.send(true)
|
||||
// DOCEND 08
|
||||
|
||||
/**----------------------------------------
|
||||
* RESPONDING TO COLLECT_SIGNATURES_FLOW *
|
||||
----------------------------------------**/
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
// The responder will often need to respond to a call to
|
||||
// ``CollectSignaturesFlow``. It does so my invoking its own
|
||||
// ``SignTransactionFlow`` subclass.
|
||||
// DOCSTART 16
|
||||
val signTransactionFlow: SignTransactionFlow = object : SignTransactionFlow(counterpartySession) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
// Any additional checking we see fit...
|
||||
val outputState = stx.tx.outputsOfType<DummyState>().single()
|
||||
require(outputState.magicNumber == 777)
|
||||
}
|
||||
}
|
||||
|
||||
val idOfTxWeSigned = subFlow(signTransactionFlow).id
|
||||
// DOCEND 16
|
||||
|
||||
/**-----------------------------
|
||||
* FINALISING THE TRANSACTION *
|
||||
-----------------------------**/
|
||||
progressTracker.currentStep = FINALISATION
|
||||
|
||||
// As the final step the responder waits to receive the notarised transaction from the sending party
|
||||
// Since it knows the ID of the transaction it just signed, the transaction ID is specified to ensure the correct
|
||||
// transaction is received and recorded.
|
||||
// DOCSTART ReceiveFinalityFlow
|
||||
subFlow(ReceiveFinalityFlow(counterpartySession, expectedTxId = idOfTxWeSigned))
|
||||
// DOCEND ReceiveFinalityFlow
|
||||
}
|
||||
}
|
@ -1,259 +0,0 @@
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.ReceiveFinalityFlow
|
||||
import net.corda.core.flows.ReceiveStateAndRefFlow
|
||||
import net.corda.core.flows.ReceiveTransactionFlow
|
||||
import net.corda.core.flows.SendStateAndRefFlow
|
||||
import net.corda.core.flows.SendTransactionFlow
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.builder
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.schemas.CashSchemaV1
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import java.util.*
|
||||
|
||||
@CordaSerializable
|
||||
private data class FxRequest(val tradeId: String,
|
||||
val amount: Amount<Issued<Currency>>,
|
||||
val owner: Party,
|
||||
val counterparty: Party,
|
||||
val notary: Party)
|
||||
|
||||
// DOCSTART 1
|
||||
// This is equivalent to the Cash.generateSpend
|
||||
// Which is brought here to make the filtering logic more visible in the example
|
||||
private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
lockId: UUID,
|
||||
amountRequired: Amount<Issued<Currency>>,
|
||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||
// extract our identity for convenience
|
||||
val ourKeys = serviceHub.keyManagementService.keys
|
||||
val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") }
|
||||
val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties)
|
||||
|
||||
val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = listOf(notary as AbstractParty))
|
||||
|
||||
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) }
|
||||
val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression)
|
||||
|
||||
val fullCriteria = fungibleCriteria.and(vaultCriteria).and(cashCriteria)
|
||||
|
||||
val eligibleStates = serviceHub.vaultService.tryLockFungibleStatesForSpending(lockId, fullCriteria, amountRequired.withoutIssuer(), Cash.State::class.java)
|
||||
|
||||
check(eligibleStates.isNotEmpty()) { "Insufficient funds" }
|
||||
val amount = eligibleStates.fold(0L) { tot, (state) -> tot + state.data.amount.quantity }
|
||||
val change = amount - amountRequired.quantity
|
||||
|
||||
return Pair(eligibleStates, change)
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, lockId: UUID, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> {
|
||||
// Create amount with correct issuer details
|
||||
val sellAmount = request.amount
|
||||
|
||||
// DOCSTART 2
|
||||
// Gather our inputs. We would normally use VaultService.generateSpend
|
||||
// to carry out the build in a single step. To be more explicit
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevents issues when
|
||||
// the flow is suspended.
|
||||
val (inputs, residual) = gatherOurInputs(serviceHub, lockId, sellAmount, request.notary)
|
||||
|
||||
// Build and an output state for the counterparty
|
||||
val transferredFundsOutput = Cash.State(sellAmount, request.counterparty)
|
||||
|
||||
val outputs = if (residual > 0L) {
|
||||
// Build an output state for the residual change back to us
|
||||
val residualAmount = Amount(residual, sellAmount.token)
|
||||
val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.singleIdentity())
|
||||
listOf(transferredFundsOutput, residualOutput)
|
||||
} else {
|
||||
listOf(transferredFundsOutput)
|
||||
}
|
||||
return Pair(inputs, outputs)
|
||||
// DOCEND 2
|
||||
}
|
||||
|
||||
// A flow representing creating a transaction that
|
||||
// carries out exchange of cash assets.
|
||||
@InitiatingFlow
|
||||
class ForeignExchangeFlow(private val tradeId: String,
|
||||
private val baseCurrencyAmount: Amount<Issued<Currency>>,
|
||||
private val quoteCurrencyAmount: Amount<Issued<Currency>>,
|
||||
private val counterparty: Party,
|
||||
private val weAreBaseCurrencySeller: Boolean,
|
||||
private val notary: Party) : FlowLogic<SecureHash>() {
|
||||
@Suspendable
|
||||
override fun call(): SecureHash {
|
||||
// Select correct sides of the Fx exchange to query for.
|
||||
// Specifically we own the assets we wish to sell.
|
||||
// Also prepare the other side query
|
||||
val (localRequest, remoteRequest) = if (weAreBaseCurrencySeller) {
|
||||
val local = FxRequest(tradeId, baseCurrencyAmount, ourIdentity, counterparty, notary)
|
||||
val remote = FxRequest(tradeId, quoteCurrencyAmount, counterparty, ourIdentity, notary)
|
||||
Pair(local, remote)
|
||||
} else {
|
||||
val local = FxRequest(tradeId, quoteCurrencyAmount, ourIdentity, counterparty, notary)
|
||||
val remote = FxRequest(tradeId, baseCurrencyAmount, counterparty, ourIdentity, notary)
|
||||
Pair(local, remote)
|
||||
}
|
||||
|
||||
// Call the helper method to identify suitable inputs and make the outputs
|
||||
val (ourInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest)
|
||||
|
||||
// identify the notary for our states
|
||||
val notary = ourInputStates.first().state.notary
|
||||
// ensure request to other side is for a consistent notary
|
||||
val remoteRequestWithNotary = remoteRequest.copy(notary = notary)
|
||||
|
||||
// Send the request to the counterparty to verify and call their version of prepareOurInputsAndOutputs
|
||||
// Then they can return their candidate states
|
||||
val counterpartySession = initiateFlow(counterparty)
|
||||
counterpartySession.send(remoteRequestWithNotary)
|
||||
val theirInputStates = subFlow(ReceiveStateAndRefFlow<Cash.State>(counterpartySession))
|
||||
val theirOutputStates = counterpartySession.receive<List<Cash.State>>().unwrap {
|
||||
require(theirInputStates.all { it.state.notary == notary }) {
|
||||
"notary of remote states must be same as for our states"
|
||||
}
|
||||
require(theirInputStates.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Inputs not of the correct currency"
|
||||
}
|
||||
require(it.all { it.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
"Outputs not of the correct currency"
|
||||
}
|
||||
require(theirInputStates.map { it.state.data.amount.quantity }.sum()
|
||||
>= remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided inputs don't provide sufficient funds"
|
||||
}
|
||||
val sum = it.filter { it.owner.let { it is Party && serviceHub.myInfo.isLegalIdentity(it) } }.map { it.amount.quantity }.sum()
|
||||
require(sum == remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided outputs don't provide the request quantity"
|
||||
}
|
||||
it // return validated response
|
||||
}
|
||||
|
||||
// having collated the data create the full transaction.
|
||||
val signedTransaction = buildTradeProposal(ourInputStates, ourOutputStates, theirInputStates, theirOutputStates)
|
||||
|
||||
// pass transaction details to the counterparty to revalidate and confirm with a signature
|
||||
// Allow counterparty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(counterpartySession, signedTransaction))
|
||||
val allPartySignedTx = counterpartySession.receive<TransactionSignature>().unwrap {
|
||||
val withNewSignature = signedTransaction + it
|
||||
// check all signatures are present except the notary
|
||||
withNewSignature.verifySignaturesExcept(withNewSignature.tx.notary!!.owningKey)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
// In a full solution there would be states tracking the trade request which
|
||||
// would be included in the transaction and enforce the amounts and tradeId
|
||||
withNewSignature.tx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
withNewSignature // return the almost complete transaction
|
||||
}
|
||||
|
||||
// Initiate the standard protocol to notarise and distribute to the involved parties.
|
||||
subFlow(FinalityFlow(allPartySignedTx, counterpartySession))
|
||||
|
||||
return allPartySignedTx.id
|
||||
}
|
||||
|
||||
// DOCSTART 3
|
||||
private fun buildTradeProposal(ourInputStates: List<StateAndRef<Cash.State>>,
|
||||
ourOutputState: List<Cash.State>,
|
||||
theirInputStates: List<StateAndRef<Cash.State>>,
|
||||
theirOutputState: List<Cash.State>): SignedTransaction {
|
||||
// This is the correct way to create a TransactionBuilder,
|
||||
// do not construct directly.
|
||||
// We also set the notary to match the input notary
|
||||
val builder = TransactionBuilder(ourInputStates.first().state.notary)
|
||||
|
||||
// Add the move commands and key to indicate all the respective owners and need to sign
|
||||
val ourSigners = ourInputStates.map { it.state.data.owner.owningKey }.toSet()
|
||||
val theirSigners = theirInputStates.map { it.state.data.owner.owningKey }.toSet()
|
||||
builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList())
|
||||
|
||||
// Build and add the inputs and outputs
|
||||
builder.withItems(*ourInputStates.toTypedArray())
|
||||
builder.withItems(*theirInputStates.toTypedArray())
|
||||
builder.withItems(*ourOutputState.map { StateAndContract(it, Cash.PROGRAM_ID) }.toTypedArray())
|
||||
builder.withItems(*theirOutputState.map { StateAndContract(it, Cash.PROGRAM_ID) }.toTypedArray())
|
||||
|
||||
// We have already validated their response and trust our own data
|
||||
// so we can sign. Note the returned SignedTransaction is still not fully signed
|
||||
// and would not pass full verification yet.
|
||||
return serviceHub.signInitialTransaction(builder, ourSigners.single())
|
||||
}
|
||||
// DOCEND 3
|
||||
}
|
||||
|
||||
@InitiatedBy(ForeignExchangeFlow::class)
|
||||
class ForeignExchangeRemoteFlow(private val source: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// Initial receive from remote party
|
||||
val request = source.receive<FxRequest>().unwrap {
|
||||
// We would need to check that this is a known trade ID here!
|
||||
// Also that the amounts and source are correct with the trade details.
|
||||
// In a production system there would be other Corda contracts tracking
|
||||
// the lifecycle of the Fx trades which would be included in the transaction
|
||||
|
||||
// Check request is for us
|
||||
require(serviceHub.myInfo.isLegalIdentity(it.owner)) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
require(source.counterparty == it.counterparty) {
|
||||
"Request does not include the correct counterparty"
|
||||
}
|
||||
it // return validated request
|
||||
}
|
||||
|
||||
// Gather our inputs. We would normally use VaultService.generateSpend
|
||||
// to carry out the build in a single step. To be more explicit
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevent issues when
|
||||
// the flow is suspended.
|
||||
val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, request)
|
||||
|
||||
// Send back our proposed states and await the full transaction to verify
|
||||
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single()
|
||||
// SendStateAndRefFlow allows counterparty to access our transaction data to resolve the transaction.
|
||||
subFlow(SendStateAndRefFlow(source, ourInputState))
|
||||
source.send(ourOutputState)
|
||||
val proposedTrade = subFlow(ReceiveTransactionFlow(source, checkSufficientSignatures = false)).let {
|
||||
val wtx = it.tx
|
||||
// check all signatures are present except our own and the notary
|
||||
it.verifySignaturesExcept(ourKey, wtx.notary!!.owningKey)
|
||||
it // return the SignedTransaction
|
||||
}
|
||||
|
||||
// assuming we have completed state and business level validation we can sign the trade
|
||||
val ourSignature = serviceHub.createSignature(proposedTrade, ourKey)
|
||||
|
||||
// send the other side our signature.
|
||||
source.send(ourSignature)
|
||||
|
||||
// and then finally stored the finalised transaction into our vault
|
||||
subFlow(ReceiveFinalityFlow(source))
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
@file:Suppress("DEPRECATION", "MemberVisibilityCanBePrivate", "unused")
|
||||
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
// DOCSTART LaunchSpaceshipFlow
|
||||
@InitiatingFlow
|
||||
class LaunchSpaceshipFlow : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val shouldLaunchSpaceship = receive<Boolean>(getPresident()).unwrap { it }
|
||||
if (shouldLaunchSpaceship) {
|
||||
launchSpaceship()
|
||||
}
|
||||
}
|
||||
|
||||
fun launchSpaceship() {
|
||||
}
|
||||
|
||||
fun getPresident(): Party {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(LaunchSpaceshipFlow::class)
|
||||
@InitiatingFlow
|
||||
class PresidentSpaceshipFlow(val launcher: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val needCoffee = true
|
||||
send(getSecretary(), needCoffee)
|
||||
val shouldLaunchSpaceship = false
|
||||
send(launcher, shouldLaunchSpaceship)
|
||||
}
|
||||
|
||||
fun getSecretary(): Party {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(PresidentSpaceshipFlow::class)
|
||||
class SecretaryFlow(val president: Party) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
// DOCEND LaunchSpaceshipFlow
|
||||
|
||||
// DOCSTART LaunchSpaceshipFlowCorrect
|
||||
@InitiatingFlow
|
||||
class LaunchSpaceshipFlowCorrect : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val presidentSession = initiateFlow(getPresident())
|
||||
val shouldLaunchSpaceship = presidentSession.receive<Boolean>().unwrap { it }
|
||||
if (shouldLaunchSpaceship) {
|
||||
launchSpaceship()
|
||||
}
|
||||
}
|
||||
|
||||
fun launchSpaceship() {
|
||||
}
|
||||
|
||||
fun getPresident(): Party {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(LaunchSpaceshipFlowCorrect::class)
|
||||
@InitiatingFlow
|
||||
class PresidentSpaceshipFlowCorrect(val launcherSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val needCoffee = true
|
||||
val secretarySession = initiateFlow(getSecretary())
|
||||
secretarySession.send(needCoffee)
|
||||
val shouldLaunchSpaceship = false
|
||||
launcherSession.send(shouldLaunchSpaceship)
|
||||
}
|
||||
|
||||
fun getSecretary(): Party {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(PresidentSpaceshipFlowCorrect::class)
|
||||
class SecretaryFlowCorrect(val presidentSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
// DOCEND LaunchSpaceshipFlowCorrect
|
@ -1,33 +0,0 @@
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
// DOCSTART 1
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetworkParameters
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import net.corda.testing.node.TestCordapp.Companion.findCordapp
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
|
||||
class MockNetworkTestsTutorial {
|
||||
|
||||
private val mockNet = MockNetwork(MockNetworkParameters(listOf(findCordapp("com.mycordapp.package"))))
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
// DOCSTART 2
|
||||
private lateinit var nodeA: StartedMockNode
|
||||
private lateinit var nodeB: StartedMockNode
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
nodeA = mockNet.createNode()
|
||||
// We can optionally give the node a name.
|
||||
nodeB = mockNet.createNode(CordaX500Name("Bank B", "London", "GB"))
|
||||
}
|
||||
// DOCEND 2
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
package net.corda.docs.kotlin.tutorial.contract
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.NullKeys
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.finance.contracts.utils.sumCashBy
|
||||
import net.corda.finance.workflows.asset.CashUtils
|
||||
import net.corda.testing.core.singleIdentityAndCert
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class CommercialPaper : Contract {
|
||||
// DOCSTART 8
|
||||
companion object {
|
||||
const val CP_PROGRAM_ID: ContractClassName = "net.corda.finance.contracts.CommercialPaper"
|
||||
}
|
||||
// DOCEND 8
|
||||
|
||||
// DOCSTART 3
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
// Group by everything except owner: any modification to the CP at all is considered changing it fundamentally.
|
||||
val groups = tx.groupStates(State::withoutOwner)
|
||||
|
||||
// There are two possible things that can be done with this CP. The first is trading it. The second is redeeming
|
||||
// it for cash on or after the maturity date.
|
||||
val command = tx.commands.requireSingleCommand<CommercialPaper.Commands>()
|
||||
// DOCEND 3
|
||||
|
||||
// DOCSTART 4
|
||||
val timeWindow: TimeWindow? = tx.timeWindow
|
||||
|
||||
for ((inputs, outputs, _) in groups) {
|
||||
when (command.value) {
|
||||
is Commands.Move -> {
|
||||
val input = inputs.single()
|
||||
requireThat {
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
"the state is propagated" using (outputs.size == 1)
|
||||
// Don't need to check anything else, as if outputs.size == 1 then the output is equal to
|
||||
// the input ignoring the owner field due to the grouping.
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Redeem -> {
|
||||
// Redemption of the paper requires movement of on-ledger cash.
|
||||
val input = inputs.single()
|
||||
val received = tx.outputs.map { it.data }.sumCashBy(input.owner)
|
||||
val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must be timestamped")
|
||||
requireThat {
|
||||
"the paper must have matured" using (time >= input.maturityDate)
|
||||
"the received amount equals the face value" using (received == input.faceValue)
|
||||
"the paper must be destroyed" using outputs.isEmpty()
|
||||
"the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.Issue -> {
|
||||
val output = outputs.single()
|
||||
val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must be timestamped")
|
||||
requireThat {
|
||||
// Don't allow people to issue commercial paper under other entities identities.
|
||||
"output states are issued by a command signer" using (output.issuance.party.owningKey in command.signers)
|
||||
"output values sum to more than the inputs" using (output.faceValue.quantity > 0)
|
||||
"the maturity date is not in the past" using (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
"can't reissue an existing state" using inputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unrecognised command")
|
||||
}
|
||||
}
|
||||
// DOCEND 4
|
||||
}
|
||||
|
||||
// DOCSTART 2
|
||||
interface Commands : CommandData {
|
||||
class Move : TypeOnlyCommandData(), Commands
|
||||
class Redeem : TypeOnlyCommandData(), Commands
|
||||
class Issue : TypeOnlyCommandData(), Commands
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
// DOCSTART 5
|
||||
fun generateIssue(issuance: PartyAndReference, faceValue: Amount<Issued<Currency>>, maturityDate: Instant,
|
||||
notary: Party): TransactionBuilder {
|
||||
val state = State(issuance, issuance.party, faceValue, maturityDate)
|
||||
val stateAndContract = StateAndContract(state, CP_PROGRAM_ID)
|
||||
return TransactionBuilder(notary = notary).withItems(stateAndContract, Command(Commands.Issue(), issuance.party.owningKey))
|
||||
}
|
||||
// DOCEND 5
|
||||
|
||||
// DOCSTART 6
|
||||
fun generateMove(tx: TransactionBuilder, paper: StateAndRef<State>, newOwner: AbstractParty) {
|
||||
tx.addInputState(paper)
|
||||
val outputState = paper.state.data.withNewOwner(newOwner).ownableState
|
||||
tx.addOutputState(outputState, CP_PROGRAM_ID)
|
||||
tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey))
|
||||
}
|
||||
// DOCEND 6
|
||||
|
||||
// DOCSTART 7
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub) {
|
||||
// Add the cash movement using the states in our vault.
|
||||
CashUtils.generateSpend(
|
||||
services = services,
|
||||
tx = tx,
|
||||
amount = paper.state.data.faceValue.withoutIssuer(),
|
||||
ourIdentity = services.myInfo.singleIdentityAndCert(),
|
||||
to = paper.state.data.owner
|
||||
)
|
||||
tx.addInputState(paper)
|
||||
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey))
|
||||
}
|
||||
// DOCEND 7
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
data class State(
|
||||
val issuance: PartyAndReference,
|
||||
override val owner: AbstractParty,
|
||||
val faceValue: Amount<Issued<Currency>>,
|
||||
val maturityDate: Instant
|
||||
) : OwnableState {
|
||||
override val participants = listOf(owner)
|
||||
|
||||
fun withoutOwner() = copy(owner = AnonymousParty(NullKeys.NullPublicKey))
|
||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(CommercialPaper.Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
// DOCEND 1
|
@ -1,31 +0,0 @@
|
||||
package net.corda.docs.kotlin.tutorial.flowstatemachines
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowExternalAsyncOperation
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class SummingOperation(val a: Int, val b: Int) : FlowExternalAsyncOperation<Int> {
|
||||
override fun execute(deduplicationId: String): CompletableFuture<Int> {
|
||||
return CompletableFuture.completedFuture(a + b)
|
||||
}
|
||||
}
|
||||
|
||||
// DOCSTART SummingOperationThrowing
|
||||
class SummingOperationThrowing(val a: Int, val b: Int) : FlowExternalAsyncOperation<Int> {
|
||||
override fun execute(deduplicationId: String): CompletableFuture<Int> {
|
||||
throw IllegalStateException("You shouldn't be calling me")
|
||||
}
|
||||
}
|
||||
// DOCEND SummingOperationThrowing
|
||||
|
||||
@StartableByRPC
|
||||
class ExampleSummingFlow : FlowLogic<Int>() {
|
||||
@Suspendable
|
||||
override fun call(): Int {
|
||||
val answer = await(SummingOperation(1, 2))
|
||||
return answer // hopefully 3
|
||||
}
|
||||
}
|
||||
|
@ -1,66 +0,0 @@
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.flowstatemachines
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.OwnableState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.finance.flows.TwoPartyTradeFlow
|
||||
import java.util.*
|
||||
|
||||
// DOCSTART 1
|
||||
object TwoPartyTradeFlow {
|
||||
class UnacceptablePriceException(givenPrice: Amount<Currency>) : FlowException("Unacceptable price: $givenPrice")
|
||||
class AssetMismatchException(val expectedTypeName: String, val typeName: String) : FlowException() {
|
||||
override fun toString() = "The submitted asset didn't match the expected type: $expectedTypeName vs $typeName"
|
||||
}
|
||||
|
||||
/**
|
||||
* This object is serialised to the network and is the first flow message the seller sends to the buyer.
|
||||
*
|
||||
* @param payToIdentity anonymous identity of the seller, for payment to be sent to.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class SellerTradeInfo(
|
||||
val price: Amount<Currency>,
|
||||
val payToIdentity: PartyAndCertificate
|
||||
)
|
||||
|
||||
open class Seller(private val otherSideSession: FlowSession,
|
||||
private val assetToSell: StateAndRef<OwnableState>,
|
||||
private val price: Amount<Currency>,
|
||||
private val myParty: PartyAndCertificate,
|
||||
override val progressTracker: ProgressTracker = TwoPartyTradeFlow.Seller.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
fun tracker() = ProgressTracker()
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
open class Buyer(private val sellerSession: FlowSession,
|
||||
private val notary: Party,
|
||||
private val acceptablePrice: Amount<Currency>,
|
||||
private val typeToBuy: Class<out OwnableState>,
|
||||
private val anonymous: Boolean) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
@ -1,53 +0,0 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.helloworld
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.template.TemplateContract
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
// DOCSTART 01
|
||||
// Add these imports:
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
|
||||
// Replace Initiator's definition with:
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class IOUFlow(val iouValue: Int,
|
||||
val otherParty: Party) : FlowLogic<Unit>() {
|
||||
|
||||
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
|
||||
override val progressTracker = ProgressTracker()
|
||||
|
||||
/** The flow logic is encapsulated within the call() method. */
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// We retrieve the notary identity from the network map.
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||
|
||||
// We create the transaction components.
|
||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||
val command = Command(TemplateContract.Commands.Action(), ourIdentity.owningKey)
|
||||
|
||||
// We create a transaction builder and add the components.
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
.addOutputState(outputState, TemplateContract.ID)
|
||||
.addCommand(command)
|
||||
|
||||
// We sign the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
// Creating a session with the other party.
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
|
||||
// We finalise the transaction and then send it to the counterparty.
|
||||
subFlow(FinalityFlow(signedTx, otherPartySession))
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -1,20 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.helloworld
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.ReceiveFinalityFlow
|
||||
|
||||
// DOCSTART 01
|
||||
// Replace Responder's definition with:
|
||||
@InitiatedBy(IOUFlow::class)
|
||||
class IOUFlowResponder(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherPartySession))
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -1,17 +0,0 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.helloworld
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
|
||||
// DOCSTART 01
|
||||
// Add this import:
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
// Replace TemplateState's definition with:
|
||||
class IOUState(val value: Int,
|
||||
val lender: Party,
|
||||
val borrower: Party) : ContractState {
|
||||
override val participants get() = listOf(lender, borrower)
|
||||
}
|
||||
// DOCEND 01
|
@ -1,47 +0,0 @@
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.tearoffs
|
||||
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.MerkleTreeException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.FilteredTransactionVerificationException
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.finance.contracts.Fix
|
||||
import java.util.function.Predicate
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// Type alias to make the example coherent.
|
||||
val oracle = Any() as AbstractParty
|
||||
val stx = Any() as SignedTransaction
|
||||
|
||||
// DOCSTART 1
|
||||
val filtering = Predicate<Any> {
|
||||
when (it) {
|
||||
is Command<*> -> oracle.owningKey in it.signers && it.value is Fix
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
// DOCSTART 2
|
||||
val ftx: FilteredTransaction = stx.buildFilteredTransaction(filtering)
|
||||
// DOCEND 2
|
||||
|
||||
// DOCSTART 3
|
||||
// Direct access to included commands, inputs, outputs, attachments etc.
|
||||
val cmds: List<Command<*>> = ftx.commands
|
||||
val ins: List<StateRef> = ftx.inputs
|
||||
val timeWindow: TimeWindow? = ftx.timeWindow
|
||||
// ...
|
||||
// DOCEND 3
|
||||
|
||||
try {
|
||||
ftx.verify()
|
||||
} catch (e: FilteredTransactionVerificationException) {
|
||||
throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.")
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package net.corda.docs.kotlin.tutorial.twoparty
|
||||
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
// DOCSTART 01
|
||||
// Add this import:
|
||||
import net.corda.core.contracts.*
|
||||
|
||||
class IOUContract : Contract {
|
||||
companion object {
|
||||
const val ID = "com.template.IOUContract"
|
||||
}
|
||||
|
||||
// Our Create command.
|
||||
class Create : CommandData
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commands.requireSingleCommand<Create>()
|
||||
|
||||
requireThat {
|
||||
// Constraints on the shape of the transaction.
|
||||
"No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
|
||||
"There should be one output state of type IOUState." using (tx.outputs.size == 1)
|
||||
|
||||
// IOU-specific constraints.
|
||||
val output = tx.outputsOfType<IOUState>().single()
|
||||
"The IOU's value must be non-negative." using (output.value > 0)
|
||||
"The lender and the borrower cannot be the same entity." using (output.lender != output.borrower)
|
||||
|
||||
// Constraints on the signers.
|
||||
val expectedSigners = listOf(output.borrower.owningKey, output.lender.owningKey)
|
||||
"There must be two signers." using (command.signers.toSet().size == 2)
|
||||
"The borrower and lender must be signers." using (command.signers.containsAll(expectedSigners))
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 01
|
@ -1,58 +0,0 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.twoparty
|
||||
|
||||
// DOCSTART 01
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.flows.CollectSignaturesFlow
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
// DOCEND 01
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class IOUFlow(val iouValue: Int,
|
||||
val otherParty: Party) : FlowLogic<Unit>() {
|
||||
|
||||
/** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
|
||||
override val progressTracker = ProgressTracker()
|
||||
|
||||
/** The flow logic is encapsulated within the call() method. */
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// DOCSTART 02
|
||||
// We retrieve the notary identity from the network map.
|
||||
val notary = serviceHub.networkMapCache.notaryIdentities[0]
|
||||
|
||||
// We create the transaction components.
|
||||
val outputState = IOUState(iouValue, ourIdentity, otherParty)
|
||||
val command = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
|
||||
|
||||
// We create a transaction builder and add the components.
|
||||
val txBuilder = TransactionBuilder(notary = notary)
|
||||
.addOutputState(outputState, IOUContract.ID)
|
||||
.addCommand(command)
|
||||
|
||||
// Verifying the transaction.
|
||||
txBuilder.verify(serviceHub)
|
||||
|
||||
// Signing the transaction.
|
||||
val signedTx = serviceHub.signInitialTransaction(txBuilder)
|
||||
|
||||
// Creating a session with the other party.
|
||||
val otherPartySession = initiateFlow(otherParty)
|
||||
|
||||
// Obtaining the counterparty's signature.
|
||||
val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherPartySession), CollectSignaturesFlow.tracker()))
|
||||
|
||||
// Finalising the transaction.
|
||||
subFlow(FinalityFlow(fullySignedTx, otherPartySession))
|
||||
// DOCEND 02
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.twoparty
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow
|
||||
import net.corda.docs.kotlin.tutorial.helloworld.IOUState
|
||||
|
||||
// Add these imports:
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
|
||||
// Define IOUFlowResponder:
|
||||
@InitiatedBy(IOUFlow::class)
|
||||
class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||
// DOCSTART 1
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherPartySession) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = requireThat {
|
||||
val output = stx.tx.outputs.single().data
|
||||
"This must be an IOU transaction." using (output is IOUState)
|
||||
val iou = output as IOUState
|
||||
"The IOU's value can't be too high." using (iou.value < 100)
|
||||
}
|
||||
}
|
||||
val expectedTxId = subFlow(signTransactionFlow).id
|
||||
subFlow(ReceiveFinalityFlow(otherPartySession, expectedTxId))
|
||||
}
|
||||
// DOCEND 1
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package net.corda.docs.kotlin.tutorial.twoparty
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.Party
|
||||
|
||||
class IOUState(val value: Int,
|
||||
val lender: Party,
|
||||
val borrower: Party) : ContractState {
|
||||
override val participants get() = listOf(lender, borrower)
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package net.corda.docs.kotlin.txbuild
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.contracts.requireSingleCommand
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.ReceiveFinalityFlow
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
// Minimal state model of a manual approval process
|
||||
@CordaSerializable
|
||||
enum class WorkflowState {
|
||||
NEW,
|
||||
APPROVED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
const val TRADE_APPROVAL_PROGRAM_ID = "net.corda.docs.kotlin.txbuild.TradeApprovalContract"
|
||||
|
||||
/**
|
||||
* Minimal contract to encode a simple workflow with one initial state and two possible eventual states.
|
||||
* It is assumed one party unilaterally submits and the other manually retrieves the deal and completes it.
|
||||
*/
|
||||
data class TradeApprovalContract(val blank: Unit? = null) : Contract {
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Issue : TypeOnlyCommandData(), Commands // Record receipt of deal details
|
||||
class Completed : TypeOnlyCommandData(), Commands // Record match
|
||||
}
|
||||
|
||||
/**
|
||||
* Truly minimal state that just records a tradeId string and the parties involved.
|
||||
*/
|
||||
data class State(val tradeId: String,
|
||||
val source: Party,
|
||||
val counterparty: Party,
|
||||
val state: WorkflowState = WorkflowState.NEW,
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(tradeId)) : LinearState {
|
||||
override val participants: List<AbstractParty> get() = listOf(source, counterparty)
|
||||
}
|
||||
|
||||
/**
|
||||
* The verify method locks down the allowed transactions to contain just a single proposal being
|
||||
* created/modified and the only modification allowed is to the state field.
|
||||
*/
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val command = tx.commands.requireSingleCommand<TradeApprovalContract.Commands>()
|
||||
requireNotNull(tx.timeWindow) { "must have a time-window" }
|
||||
when (command.value) {
|
||||
is Commands.Issue -> {
|
||||
requireThat {
|
||||
"Issue of new WorkflowContract must not include any inputs" using (tx.inputs.isEmpty())
|
||||
"Issue of new WorkflowContract must be in a unique transaction" using (tx.outputs.size == 1)
|
||||
}
|
||||
val issued = tx.outputsOfType<TradeApprovalContract.State>().single()
|
||||
requireThat {
|
||||
"Issue requires the source Party as signer" using (command.signers.contains(issued.source.owningKey))
|
||||
"Initial Issue state must be NEW" using (issued.state == WorkflowState.NEW)
|
||||
}
|
||||
}
|
||||
is Commands.Completed -> {
|
||||
val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId }
|
||||
require(stateGroups.size == 1) { "Must be only a single proposal in transaction" }
|
||||
for ((inputs, outputs) in stateGroups) {
|
||||
val before = inputs.single()
|
||||
val after = outputs.single()
|
||||
requireThat {
|
||||
"Only a non-final trade can be modified" using (before.state == WorkflowState.NEW)
|
||||
"Output must be a final state" using (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED))
|
||||
"Completed command can only change state" using (before == after.copy(state = before.state))
|
||||
"Completed command requires the source Party as signer" using (command.signers.contains(before.source.owningKey))
|
||||
"Completed command requires the counterparty as signer" using (command.signers.contains(before.counterparty.owningKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unrecognised Command $command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to create a workflow state, sign and notarise it.
|
||||
* The protocol then sends a copy to the other node. We don't require the other party to sign
|
||||
* as their approval/rejection is to follow.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class SubmitTradeApprovalFlow(private val tradeId: String,
|
||||
private val counterparty: Party,
|
||||
private val notary: Party) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<TradeApprovalContract.State> {
|
||||
// Manufacture an initial state
|
||||
val tradeProposal = TradeApprovalContract.State(tradeId, ourIdentity, counterparty)
|
||||
// Create the TransactionBuilder and populate with the new state.
|
||||
val tx = TransactionBuilder(notary).withItems(
|
||||
StateAndContract(tradeProposal, TRADE_APPROVAL_PROGRAM_ID),
|
||||
Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey)))
|
||||
tx.setTimeWindow(serviceHub.clock.instant(), 60.seconds)
|
||||
// We can automatically sign as there is no untrusted data.
|
||||
val signedTx = serviceHub.signInitialTransaction(tx)
|
||||
// Notarise and distribute.
|
||||
subFlow(FinalityFlow(signedTx, initiateFlow(counterparty)))
|
||||
// Return the initial state
|
||||
return signedTx.tx.outRef(0)
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(SubmitTradeApprovalFlow::class)
|
||||
class SubmitTradeApprovalResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(ReceiveFinalityFlow(otherSide))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to complete a proposal submitted by another party and ensure both nodes
|
||||
* end up with a fully signed copy of the state either as APPROVED, or REJECTED
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class SubmitCompletionFlow(private val ref: StateRef, private val verdict: WorkflowState) : FlowLogic<StateAndRef<TradeApprovalContract.State>>() {
|
||||
init {
|
||||
require(verdict in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED)) {
|
||||
"Verdict must be a final state"
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): StateAndRef<TradeApprovalContract.State> {
|
||||
// DOCSTART 1
|
||||
val criteria = VaultQueryCriteria(stateRefs = listOf(ref))
|
||||
val latestRecord = serviceHub.vaultService.queryBy<TradeApprovalContract.State>(criteria).states.single()
|
||||
// DOCEND 1
|
||||
|
||||
// Check the protocol hasn't already been run
|
||||
require(latestRecord.ref == ref) {
|
||||
"Input trade $ref is not latest version $latestRecord"
|
||||
}
|
||||
// Require that the state is still modifiable
|
||||
require(latestRecord.state.data.state == WorkflowState.NEW) {
|
||||
"Input trade not modifiable ${latestRecord.state.data.state}"
|
||||
}
|
||||
// Check we are the correct Party to run the protocol. Note they will counter check this too.
|
||||
require(serviceHub.myInfo.isLegalIdentity(latestRecord.state.data.counterparty)) {
|
||||
"The counterparty must give the verdict"
|
||||
}
|
||||
|
||||
// DOCSTART 2
|
||||
// Modify the state field for new output. We use copy, to ensure no other modifications.
|
||||
// It is especially important for a LinearState that the linearId is copied across,
|
||||
// not accidentally assigned a new random id.
|
||||
val newState = latestRecord.state.data.copy(state = verdict)
|
||||
|
||||
// We have to use the original notary for the new transaction
|
||||
val notary = latestRecord.state.notary
|
||||
|
||||
// Get and populate the new TransactionBuilder
|
||||
// To destroy the old proposal state and replace with the new completion state.
|
||||
// Also add the Completed command with keys of all parties to signal the Tx purpose
|
||||
// to the Contract verify method.
|
||||
val tx = TransactionBuilder(notary).
|
||||
withItems(
|
||||
latestRecord,
|
||||
StateAndContract(newState, TRADE_APPROVAL_PROGRAM_ID),
|
||||
Command(TradeApprovalContract.Commands.Completed(),
|
||||
listOf(ourIdentity.owningKey, latestRecord.state.data.source.owningKey)))
|
||||
tx.setTimeWindow(serviceHub.clock.instant(), 60.seconds)
|
||||
// We can sign this transaction immediately as we have already checked all the fields and the decision
|
||||
// is ultimately a manual one from the caller.
|
||||
// As a SignedTransaction we can pass the data around certain that it cannot be modified,
|
||||
// although we do require further signatures to complete the process.
|
||||
val selfSignedTx = serviceHub.signInitialTransaction(tx)
|
||||
//DOCEND 2
|
||||
// Send the signed transaction to the originator and await their signature to confirm
|
||||
val sourceSession = initiateFlow(newState.source)
|
||||
val allPartySignedTx = sourceSession.sendAndReceive<TransactionSignature>(selfSignedTx).unwrap {
|
||||
// Add their signature to our unmodified transaction. To check they signed the same tx.
|
||||
val agreedTx = selfSignedTx + it
|
||||
// Receive back their signature and confirm that it is for an unmodified transaction
|
||||
// Also that the only missing signature is from teh Notary
|
||||
agreedTx.verifySignaturesExcept(notary.owningKey)
|
||||
// Recheck the data of the transaction. Note we run toLedgerTransaction on the WireTransaction
|
||||
// as we do not have all the signature.
|
||||
agreedTx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
// return the SignedTransaction to notarise
|
||||
agreedTx
|
||||
}
|
||||
// DOCSTART 4
|
||||
// Notarise and distribute the completed transaction.
|
||||
subFlow(FinalityFlow(allPartySignedTx, sourceSession))
|
||||
// DOCEND 4
|
||||
// Return back the details of the completed state/transaction.
|
||||
return allPartySignedTx.tx.outRef(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple flow to receive the final decision on a proposal.
|
||||
* Then after checking to sign it and eventually store the fully notarised
|
||||
* transaction to the ledger.
|
||||
*/
|
||||
@InitiatedBy(SubmitCompletionFlow::class)
|
||||
class RecordCompletionFlow(private val sourceSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
// DOCSTART 3
|
||||
// First we receive the verdict transaction signed by their single key
|
||||
val completeTx = sourceSession.receive<SignedTransaction>().unwrap {
|
||||
// Check the transaction is signed apart from our own key and the notary
|
||||
it.verifySignaturesExcept(ourIdentity.owningKey, it.tx.notary!!.owningKey)
|
||||
// Check the transaction data is correctly formed
|
||||
val ltx = it.toLedgerTransaction(serviceHub, false)
|
||||
ltx.verify()
|
||||
// Confirm that this is the expected type of transaction
|
||||
require(ltx.commands.single().value is TradeApprovalContract.Commands.Completed) {
|
||||
"Transaction must represent a workflow completion"
|
||||
}
|
||||
// Check the context dependent parts of the transaction as the
|
||||
// Contract verify method must not use serviceHub queries.
|
||||
val state = ltx.outRef<TradeApprovalContract.State>(0)
|
||||
require(serviceHub.myInfo.isLegalIdentity(state.state.data.source)) {
|
||||
"Proposal not one of our original proposals"
|
||||
}
|
||||
require(state.state.data.counterparty == sourceSession.counterparty) {
|
||||
"Proposal not for sent from correct source"
|
||||
}
|
||||
it
|
||||
}
|
||||
// DOCEND 3
|
||||
// Having verified the SignedTransaction passed to us we can sign it too
|
||||
val ourSignature = serviceHub.createSignature(completeTx)
|
||||
// Send our signature to the other party.
|
||||
sourceSession.send(ourSignature)
|
||||
|
||||
subFlow(ReceiveFinalityFlow(sourceSession))
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.corda.docs.kotlin.vault
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.finance.flows.AbstractCashFlow
|
||||
import net.corda.finance.flows.CashException
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.finance.flows.CashPaymentFlow
|
||||
import java.util.*
|
||||
|
||||
// DOCSTART CustomVaultQuery
|
||||
object CustomVaultQuery {
|
||||
|
||||
@CordaService
|
||||
class Service(val services: AppServiceHub) : SingletonSerializeAsToken() {
|
||||
private companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
fun rebalanceCurrencyReserves(): List<Amount<Currency>> {
|
||||
val nativeQuery = """
|
||||
select
|
||||
cashschema.ccy_code,
|
||||
sum(cashschema.pennies)
|
||||
from
|
||||
vault_states vaultschema
|
||||
join
|
||||
contract_cash_states cashschema
|
||||
where
|
||||
vaultschema.output_index=cashschema.output_index
|
||||
and vaultschema.transaction_id=cashschema.transaction_id
|
||||
and vaultschema.state_status=0
|
||||
group by
|
||||
cashschema.ccy_code
|
||||
order by
|
||||
sum(cashschema.pennies) desc
|
||||
"""
|
||||
log.info("SQL to execute: $nativeQuery")
|
||||
val session = services.jdbcSession()
|
||||
return session.prepareStatement(nativeQuery).use { prepStatement ->
|
||||
prepStatement.executeQuery().use { rs ->
|
||||
val topUpLimits: MutableList<Amount<Currency>> = mutableListOf()
|
||||
while (rs.next()) {
|
||||
val currencyStr = rs.getString(1)
|
||||
val amount = rs.getLong(2)
|
||||
log.info("$currencyStr : $amount")
|
||||
topUpLimits.add(Amount(amount, Currency.getInstance(currencyStr)))
|
||||
}
|
||||
topUpLimits
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND CustomVaultQuery
|
||||
|
||||
/**
|
||||
* This is a slightly modified version of the IssuerFlow, which uses a 3rd party custom query to
|
||||
* retrieve a list of currencies and top up amounts to be used in the issuance.
|
||||
*/
|
||||
object TopupIssuerFlow {
|
||||
@CordaSerializable
|
||||
data class TopupRequest(val issueToParty: Party,
|
||||
val issuerPartyRef: OpaqueBytes,
|
||||
val notaryParty: Party)
|
||||
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class TopupIssuanceRequester(val issueToParty: Party,
|
||||
val issueToPartyRef: OpaqueBytes,
|
||||
val issuerBankParty: Party,
|
||||
val notaryParty: Party) : FlowLogic<List<AbstractCashFlow.Result>>() {
|
||||
@Suspendable
|
||||
@Throws(CashException::class)
|
||||
override fun call(): List<AbstractCashFlow.Result> {
|
||||
val topupRequest = TopupRequest(issueToParty, issueToPartyRef, notaryParty)
|
||||
return initiateFlow(issuerBankParty).sendAndReceive<List<AbstractCashFlow.Result>>(topupRequest).unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(TopupIssuanceRequester::class)
|
||||
class TopupIssuer(val otherPartySession: FlowSession) : FlowLogic<List<SignedTransaction>>() {
|
||||
companion object {
|
||||
object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request")
|
||||
object ISSUING : ProgressTracker.Step("Issuing asset")
|
||||
object TRANSFERRING : ProgressTracker.Step("Transferring asset to issuance requester")
|
||||
object SENDING_TOP_UP_ISSUE_REQUEST : ProgressTracker.Step("Requesting asset issue top up")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_REQUEST, ISSUING, TRANSFERRING, SENDING_TOP_UP_ISSUE_REQUEST)
|
||||
}
|
||||
|
||||
override val progressTracker: ProgressTracker = tracker()
|
||||
|
||||
// DOCSTART TopupIssuer
|
||||
@Suspendable
|
||||
@Throws(CashException::class)
|
||||
override fun call(): List<SignedTransaction> {
|
||||
progressTracker.currentStep = AWAITING_REQUEST
|
||||
val topupRequest = otherPartySession.receive<TopupRequest>().unwrap {
|
||||
it
|
||||
}
|
||||
|
||||
val customVaultQueryService = serviceHub.cordaService(CustomVaultQuery.Service::class.java)
|
||||
val reserveLimits = customVaultQueryService.rebalanceCurrencyReserves()
|
||||
|
||||
val txns: List<SignedTransaction> = reserveLimits.map { amount ->
|
||||
// request asset issue
|
||||
logger.info("Requesting currency issue $amount")
|
||||
val txn = issueCashTo(amount, topupRequest.issueToParty, topupRequest.issuerPartyRef, topupRequest.notaryParty)
|
||||
progressTracker.currentStep = SENDING_TOP_UP_ISSUE_REQUEST
|
||||
return@map txn.stx
|
||||
}
|
||||
|
||||
otherPartySession.send(txns)
|
||||
return txns
|
||||
}
|
||||
// DOCEND TopupIssuer
|
||||
|
||||
@Suspendable
|
||||
private fun issueCashTo(amount: Amount<Currency>,
|
||||
issueTo: Party,
|
||||
issuerPartyRef: OpaqueBytes,
|
||||
notaryParty: Party): AbstractCashFlow.Result {
|
||||
// invoke Cash subflow to issue Asset
|
||||
progressTracker.currentStep = ISSUING
|
||||
val issueCashFlow = CashIssueFlow(amount, issuerPartyRef, notaryParty)
|
||||
val issueTx = subFlow(issueCashFlow)
|
||||
// NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||
// short-circuit when issuing to self
|
||||
if (serviceHub.myInfo.isLegalIdentity(issueTo))
|
||||
return issueTx
|
||||
// now invoke Cash subflow to Move issued assetType to issue requester
|
||||
progressTracker.currentStep = TRANSFERRING
|
||||
val moveCashFlow = CashPaymentFlow(amount, issueTo, anonymous = false)
|
||||
// NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger)
|
||||
return subFlow(moveCashFlow)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
myLegalName = "O=Bank A,L=London,C=GB"
|
||||
keyStorePassword = "cordacadevpass"
|
||||
trustStorePassword = "trustpass"
|
||||
crlCheckSoftFail = true
|
||||
dataSourceProperties {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence"
|
||||
dataSource.user = sa
|
||||
dataSource.password = ""
|
||||
}
|
||||
p2pAddress = "my-corda-node:10002"
|
||||
rpcSettings {
|
||||
useSsl = false
|
||||
standAloneBroker = false
|
||||
address = "my-corda-node:10003"
|
||||
adminAddress = "my-corda-node:10004"
|
||||
}
|
||||
rpcUsers = [
|
||||
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
|
||||
]
|
||||
devMode = false
|
||||
networkServices {
|
||||
doormanURL = "https://registration.example.com"
|
||||
networkMapURL = "https://cz.example.com"
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
myLegalName = "O=Bank A,L=London,C=GB"
|
||||
keyStorePassword = "cordacadevpass"
|
||||
trustStorePassword = "trustpass"
|
||||
crlCheckSoftFail = true
|
||||
dataSourceProperties {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence"
|
||||
dataSource.user = sa
|
||||
dataSource.password = ""
|
||||
}
|
||||
p2pAddress = "my-corda-node:10002"
|
||||
rpcSettings {
|
||||
useSsl = false
|
||||
standAloneBroker = false
|
||||
address = "my-corda-node:10003"
|
||||
adminAddress = "my-corda-node:10004"
|
||||
}
|
||||
rpcUsers = [
|
||||
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
|
||||
]
|
||||
devMode = true
|
@ -1,3 +0,0 @@
|
||||
nodeHostAndPort = "my-corda-node:10002"
|
||||
keyStorePassword = "cordacadevpass"
|
||||
trustStorePassword = "trustpass"
|
@ -1,334 +0,0 @@
|
||||
package net.corda.docs.java.tutorial.testdsl;
|
||||
|
||||
import kotlin.Unit;
|
||||
import net.corda.core.contracts.PartyAndReference;
|
||||
import net.corda.core.contracts.TransactionVerificationException;
|
||||
import net.corda.core.identity.CordaX500Name;
|
||||
import net.corda.finance.contracts.ICommercialPaperState;
|
||||
import net.corda.finance.contracts.JavaCommercialPaper;
|
||||
import net.corda.finance.contracts.asset.Cash;
|
||||
import net.corda.testing.core.TestIdentity;
|
||||
import net.corda.testing.node.MockServices;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static net.corda.finance.Currencies.DOLLARS;
|
||||
import static net.corda.finance.Currencies.issuedBy;
|
||||
import static net.corda.finance.contracts.JavaCommercialPaper.JCP_PROGRAM_ID;
|
||||
import static net.corda.testing.core.TestConstants.ALICE_NAME;
|
||||
import static net.corda.testing.core.TestConstants.BOB_NAME;
|
||||
import static net.corda.testing.node.MockServicesKt.makeTestIdentityService;
|
||||
import static net.corda.testing.node.NodeTestUtils.ledger;
|
||||
import static net.corda.testing.node.NodeTestUtils.transaction;
|
||||
|
||||
public class TutorialTestDSL {
|
||||
private static final TestIdentity alice = new TestIdentity(ALICE_NAME, 70L);
|
||||
// DOCSTART 14
|
||||
private static final TestIdentity bigCorp = new TestIdentity(new CordaX500Name("BigCorp", "New York", "GB"));
|
||||
// DOCEND 14
|
||||
private static final TestIdentity bob = new TestIdentity(BOB_NAME, 80L);
|
||||
private static final TestIdentity megaCorp = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB"));
|
||||
private final byte[] defaultRef = {123};
|
||||
private static final Instant TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z");
|
||||
private MockServices ledgerServices;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// DOCSTART 11
|
||||
ledgerServices = new MockServices(
|
||||
// A list of packages to scan for cordapps
|
||||
singletonList("net.corda.finance.contracts"),
|
||||
// The identity represented by this set of mock services. Defaults to a test identity.
|
||||
// You can also use the alternative parameter initialIdentityName which accepts a
|
||||
// [CordaX500Name]
|
||||
megaCorp,
|
||||
// An implementation of [IdentityService], which contains a list of all identities known
|
||||
// to the node. Use [makeTestIdentityService] which returns an implementation of
|
||||
// [InMemoryIdentityService] with the given identities
|
||||
makeTestIdentityService(megaCorp.getIdentity())
|
||||
);
|
||||
// DOCEND 11
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// DOCSTART 12
|
||||
private final MockServices simpleLedgerServices = new MockServices(
|
||||
// This is the identity of the node
|
||||
megaCorp,
|
||||
// Other identities the test node knows about
|
||||
bigCorp,
|
||||
alice
|
||||
);
|
||||
// DOCEND 12
|
||||
|
||||
// DOCSTART 1
|
||||
private ICommercialPaperState getPaper() {
|
||||
return new JavaCommercialPaper.State(
|
||||
megaCorp.ref(defaultRef),
|
||||
megaCorp.getParty(),
|
||||
issuedBy(DOLLARS(1000), megaCorp.ref(defaultRef)),
|
||||
TEST_TX_TIME.plus(7, ChronoUnit.DAYS)
|
||||
);
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
// DOCSTART 2
|
||||
// This example test will fail with this exception.
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void simpleCP() {
|
||||
ICommercialPaperState inState = getPaper();
|
||||
ledger(ledgerServices, l -> {
|
||||
l.transaction(tx -> {
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.input(JCP_PROGRAM_ID, inState);
|
||||
return tx.verifies();
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
// DOCSTART 3
|
||||
// This example test will fail with this exception.
|
||||
@Test(expected = TransactionVerificationException.ContractRejection.class)
|
||||
public void simpleCPMove() {
|
||||
ICommercialPaperState inState = getPaper();
|
||||
ledger(ledgerServices, l -> {
|
||||
l.transaction(tx -> {
|
||||
tx.input(JCP_PROGRAM_ID, inState);
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
return tx.verifies();
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 3
|
||||
|
||||
// DOCSTART 4
|
||||
@Test
|
||||
public void simpleCPMoveFails() {
|
||||
ICommercialPaperState inState = getPaper();
|
||||
ledger(ledgerServices, l -> {
|
||||
l.transaction(tx -> {
|
||||
tx.input(JCP_PROGRAM_ID, inState);
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
return tx.failsWith("the state is propagated");
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 4
|
||||
|
||||
// DOCSTART 5
|
||||
@Test
|
||||
public void simpleCPMoveSuccessAndFailure() {
|
||||
ICommercialPaperState inState = getPaper();
|
||||
ledger(ledgerServices, l -> {
|
||||
l.transaction(tx -> {
|
||||
tx.input(JCP_PROGRAM_ID, inState);
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.failsWith("the state is propagated");
|
||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.getParty()));
|
||||
return tx.verifies();
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 5
|
||||
|
||||
// DOCSTART 13
|
||||
@Test
|
||||
public void simpleCPMoveSuccess() {
|
||||
ICommercialPaperState inState = getPaper();
|
||||
ledger(ledgerServices, l -> {
|
||||
l.transaction(tx -> {
|
||||
tx.input(JCP_PROGRAM_ID, inState);
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.timeWindow(TEST_TX_TIME);
|
||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.getParty()));
|
||||
return tx.verifies();
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 13
|
||||
|
||||
// DOCSTART 6
|
||||
@Test
|
||||
public void simpleIssuanceWithTweak() {
|
||||
ledger(ledgerServices, l -> {
|
||||
l.transaction(tx -> {
|
||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.tweak(tw -> {
|
||||
tw.command(bigCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||
tw.timeWindow(TEST_TX_TIME);
|
||||
return tw.failsWith("output states are issued by a command signer");
|
||||
});
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||
tx.timeWindow(TEST_TX_TIME);
|
||||
return tx.verifies();
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 6
|
||||
|
||||
// DOCSTART 7
|
||||
@Test
|
||||
public void simpleIssuanceWithTweakTopLevelTx() {
|
||||
transaction(ledgerServices, tx -> {
|
||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper()); // Some CP is issued onto the ledger by MegaCorp.
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.tweak(tw -> {
|
||||
tw.command(bigCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||
tw.timeWindow(TEST_TX_TIME);
|
||||
return tw.failsWith("output states are issued by a command signer");
|
||||
});
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||
tx.timeWindow(TEST_TX_TIME);
|
||||
return tx.verifies();
|
||||
});
|
||||
}
|
||||
// DOCEND 7
|
||||
|
||||
// DOCSTART 8
|
||||
@Test
|
||||
public void chainCommercialPaper() {
|
||||
PartyAndReference issuer = megaCorp.ref(defaultRef);
|
||||
ledger(ledgerServices, l -> {
|
||||
l.unverifiedTransaction(tx -> {
|
||||
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
||||
new Cash.State(issuedBy(DOLLARS(900), issuer), alice.getParty()));
|
||||
tx.attachments(Cash.PROGRAM_ID);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
l.transaction("Issuance", tx -> {
|
||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper());
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.timeWindow(TEST_TX_TIME);
|
||||
return tx.verifies();
|
||||
});
|
||||
|
||||
l.transaction("Trade", tx -> {
|
||||
tx.input("paper");
|
||||
tx.input("alice's $900");
|
||||
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty()));
|
||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty()));
|
||||
tx.command(alice.getPublicKey(), new Cash.Commands.Move());
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
return tx.verifies();
|
||||
});
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 8
|
||||
|
||||
// DOCSTART 9
|
||||
@Test
|
||||
public void chainCommercialPaperDoubleSpend() {
|
||||
PartyAndReference issuer = megaCorp.ref(defaultRef);
|
||||
ledger(ledgerServices, l -> {
|
||||
l.unverifiedTransaction(tx -> {
|
||||
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
||||
new Cash.State(issuedBy(DOLLARS(900), issuer), alice.getParty()));
|
||||
tx.attachments(Cash.PROGRAM_ID);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
l.transaction("Issuance", tx -> {
|
||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper());
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.timeWindow(TEST_TX_TIME);
|
||||
return tx.verifies();
|
||||
});
|
||||
|
||||
l.transaction("Trade", tx -> {
|
||||
tx.input("paper");
|
||||
tx.input("alice's $900");
|
||||
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty()));
|
||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty()));
|
||||
tx.command(alice.getPublicKey(), new Cash.Commands.Move());
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
return tx.verifies();
|
||||
});
|
||||
|
||||
l.transaction(tx -> {
|
||||
tx.input("paper");
|
||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||
// We moved a paper to other pubkey.
|
||||
tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(bob.getParty()));
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
return tx.verifies();
|
||||
});
|
||||
l.fails();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 9
|
||||
|
||||
// DOCSTART 10
|
||||
@Test
|
||||
public void chainCommercialPaperTweak() {
|
||||
PartyAndReference issuer = megaCorp.ref(defaultRef);
|
||||
ledger(ledgerServices, l -> {
|
||||
l.unverifiedTransaction(tx -> {
|
||||
tx.output(Cash.PROGRAM_ID, "alice's $900",
|
||||
new Cash.State(issuedBy(DOLLARS(900), issuer), alice.getParty()));
|
||||
tx.attachments(Cash.PROGRAM_ID);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
l.transaction("Issuance", tx -> {
|
||||
tx.output(JCP_PROGRAM_ID, "paper", getPaper());
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Issue());
|
||||
tx.attachments(JCP_PROGRAM_ID);
|
||||
tx.timeWindow(TEST_TX_TIME);
|
||||
return tx.verifies();
|
||||
});
|
||||
|
||||
l.transaction("Trade", tx -> {
|
||||
tx.input("paper");
|
||||
tx.input("alice's $900");
|
||||
tx.output(Cash.PROGRAM_ID, "borrowed $900", new Cash.State(issuedBy(DOLLARS(900), issuer), megaCorp.getParty()));
|
||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||
tx.output(JCP_PROGRAM_ID, "alice's paper", inputPaper.withOwner(alice.getParty()));
|
||||
tx.command(alice.getPublicKey(), new Cash.Commands.Move(JavaCommercialPaper.class));
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
return tx.verifies();
|
||||
});
|
||||
|
||||
l.tweak(lw -> {
|
||||
lw.transaction(tx -> {
|
||||
tx.input("paper");
|
||||
JavaCommercialPaper.State inputPaper = l.retrieveOutput(JavaCommercialPaper.State.class, "paper");
|
||||
// We moved a paper to another pubkey.
|
||||
tx.output(JCP_PROGRAM_ID, "bob's paper", inputPaper.withOwner(bob.getParty()));
|
||||
tx.command(megaCorp.getPublicKey(), new JavaCommercialPaper.Commands.Move());
|
||||
return tx.verifies();
|
||||
});
|
||||
lw.fails();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
l.verifies();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
// DOCEND 10
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package net.corda.docs
|
||||
|
||||
import net.corda.core.internal.toPath
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.parseAsNodeConfiguration
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
|
||||
class ExampleConfigTest {
|
||||
|
||||
private fun <A : Any> readAndCheckConfigurations(vararg configFilenames: String, loadConfig: (Path) -> A) {
|
||||
configFilenames.forEach {
|
||||
println("Checking $it")
|
||||
val configFileResource = ExampleConfigTest::class.java.classLoader.getResource(it)
|
||||
val config = loadConfig(configFileResource.toPath())
|
||||
// Force the config fields as they are resolved lazily
|
||||
config.javaClass.kotlin.declaredMemberProperties.forEach { member ->
|
||||
if (member.visibility == KVisibility.PUBLIC) {
|
||||
member.get(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `example node_confs parses fine`() {
|
||||
readAndCheckConfigurations("example-node.conf") {
|
||||
val baseDirectory = Paths.get("some-example-base-dir")
|
||||
assertThat(ConfigHelper.loadConfig(baseDirectory = baseDirectory, configFile = it).parseAsNodeConfiguration().isValid).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
package net.corda.docs.kotlin
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.*
|
||||
import net.corda.finance.workflows.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FxTransactionBuildTutorialTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var nodeA: StartedMockNode
|
||||
private lateinit var nodeB: StartedMockNode
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance"))
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java)
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `Run ForeignExchangeFlow to completion`() {
|
||||
// Use NodeA as issuer and create some dollars and wait for the flow to stop
|
||||
nodeA.startFlow(CashIssueFlow(DOLLARS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
notary)).getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Using NodeB as Issuer create some pounds and wait for the flow to stop
|
||||
nodeB.startFlow(CashIssueFlow(POUNDS(1000),
|
||||
OpaqueBytes.of(0x01),
|
||||
notary)).getOrThrow()
|
||||
printBalances()
|
||||
|
||||
// Setup some futures on the vaults to await the arrival of the exchanged funds at both nodes
|
||||
val nodeAVaultUpdate = nodeA.services.vaultService.updates.toFuture()
|
||||
val nodeBVaultUpdate = nodeB.services.vaultService.updates.toFuture()
|
||||
|
||||
// Now run the actual Fx exchange and wait for the flow to finish
|
||||
nodeA.startFlow(ForeignExchangeFlow("trade1",
|
||||
POUNDS(100).issuedBy(nodeB.info.singleIdentity().ref(0x01)),
|
||||
DOLLARS(200).issuedBy(nodeA.info.singleIdentity().ref(0x01)),
|
||||
nodeB.info.singleIdentity(),
|
||||
weAreBaseCurrencySeller = false,
|
||||
notary = mockNet.defaultNotaryIdentity)).getOrThrow()
|
||||
// wait for the flow to finish and the vault updates to be done
|
||||
// Get the balances when the vault updates
|
||||
nodeAVaultUpdate.get()
|
||||
val balancesA = nodeA.transaction {
|
||||
nodeA.services.getCashBalances()
|
||||
}
|
||||
nodeBVaultUpdate.get()
|
||||
val balancesB = nodeB.transaction {
|
||||
nodeB.services.getCashBalances()
|
||||
}
|
||||
|
||||
println("BalanceA\n$balancesA")
|
||||
println("BalanceB\n$balancesB")
|
||||
// Verify the transfers occurred as expected
|
||||
assertEquals(POUNDS(100), balancesA[GBP])
|
||||
assertEquals(DOLLARS(1000 - 200), balancesA[USD])
|
||||
assertEquals(POUNDS(1000 - 100), balancesB[GBP])
|
||||
assertEquals(DOLLARS(200), balancesB[USD])
|
||||
}
|
||||
|
||||
private fun printBalances() {
|
||||
// Print out the balances
|
||||
nodeA.transaction {
|
||||
println("BalanceA\n" + nodeA.services.getCashBalances())
|
||||
}
|
||||
nodeB.transaction {
|
||||
println("BalanceB\n" + nodeB.services.getCashBalances())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,316 +0,0 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.corda.docs.kotlin.tutorial.testdsl
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.CP_PROGRAM_ID
|
||||
import net.corda.finance.contracts.CommercialPaper
|
||||
import net.corda.finance.contracts.ICommercialPaperState
|
||||
import net.corda.finance.contracts.asset.CASH
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.ledger
|
||||
import net.corda.testing.node.transaction
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
|
||||
class TutorialTestDSL {
|
||||
private companion object {
|
||||
val alice = TestIdentity(ALICE_NAME, 70)
|
||||
val bob = TestIdentity(BOB_NAME, 80)
|
||||
// DOCSTART 14
|
||||
val bigCorp = TestIdentity((CordaX500Name("BigCorp", "New York", "GB")))
|
||||
// DOCEND 14
|
||||
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||
val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB"))
|
||||
val TEST_TX_TIME: Instant = Instant.parse("2015-04-17T12:00:00.00Z")
|
||||
}
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
// DOCSTART 11
|
||||
private val ledgerServices = MockServices(
|
||||
// A list of packages to scan for cordapps
|
||||
listOf("net.corda.finance.contracts"),
|
||||
// The identity represented by this set of mock services. Defaults to a test identity.
|
||||
// You can also use the alternative parameter initialIdentityName which accepts a
|
||||
// [CordaX500Name]
|
||||
megaCorp,
|
||||
mock<IdentityService>().also {
|
||||
doReturn(megaCorp.party).whenever(it).partyFromKey(megaCorp.publicKey)
|
||||
doReturn(null).whenever(it).partyFromKey(bigCorp.publicKey)
|
||||
doReturn(null).whenever(it).partyFromKey(alice.publicKey)
|
||||
})
|
||||
// DOCEND 11
|
||||
|
||||
// DOCSTART 12
|
||||
@Suppress("unused")
|
||||
private val simpleLedgerServices = MockServices(
|
||||
// This is the identity of the node
|
||||
megaCorp,
|
||||
// Other identities the test node knows about
|
||||
bigCorp,
|
||||
alice
|
||||
)
|
||||
// DOCEND 12
|
||||
|
||||
// DOCSTART 1
|
||||
fun getPaper(): ICommercialPaperState = CommercialPaper.State(
|
||||
issuance = megaCorp.party.ref(123),
|
||||
owner = megaCorp.party,
|
||||
faceValue = 1000.DOLLARS `issued by` megaCorp.party.ref(123),
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
// DOCEND 1
|
||||
|
||||
// DOCSTART 2
|
||||
// This example test will fail with this exception.
|
||||
@Test(expected = IllegalStateException::class, timeout=300_000)
|
||||
fun simpleCP() {
|
||||
val inState = getPaper()
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
transaction {
|
||||
attachments(CP_PROGRAM_ID)
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 2
|
||||
|
||||
// DOCSTART 3
|
||||
// This example test will fail with this exception.
|
||||
@Test(expected = TransactionVerificationException.ContractRejection::class, timeout=300_000)
|
||||
fun simpleCPMove() {
|
||||
val inState = getPaper()
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
transaction {
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 3
|
||||
|
||||
// DOCSTART 4
|
||||
@Test(timeout=300_000)
|
||||
fun simpleCPMoveFails() {
|
||||
val inState = getPaper()
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
transaction {
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
`fails with`("the state is propagated")
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 4
|
||||
|
||||
// DOCSTART 5
|
||||
@Test(timeout=300_000)
|
||||
fun simpleCPMoveFailureAndSuccess() {
|
||||
val inState = getPaper()
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
transaction {
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
`fails with`("the state is propagated")
|
||||
output(CP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.party))
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 5
|
||||
|
||||
// DOCSTART 13
|
||||
@Test(timeout=300_000)
|
||||
fun simpleCPMoveSuccess() {
|
||||
val inState = getPaper()
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
transaction {
|
||||
input(CP_PROGRAM_ID, inState)
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
timeWindow(TEST_TX_TIME)
|
||||
output(CP_PROGRAM_ID, "alice's paper", inState.withOwner(alice.party))
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 13
|
||||
|
||||
// DOCSTART 6
|
||||
@Test(timeout=300_000)
|
||||
fun `simple issuance with tweak`() {
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
transaction {
|
||||
output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp.
|
||||
attachments(CP_PROGRAM_ID)
|
||||
tweak {
|
||||
// The wrong pubkey.
|
||||
command(bigCorp.publicKey, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
`fails with`("output states are issued by a command signer")
|
||||
}
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 6
|
||||
|
||||
// DOCSTART 7
|
||||
@Test(timeout=300_000)
|
||||
fun `simple issuance with tweak and top level transaction`() {
|
||||
ledgerServices.transaction(dummyNotary.party) {
|
||||
output(CP_PROGRAM_ID, "paper", getPaper()) // Some CP is issued onto the ledger by MegaCorp.
|
||||
attachments(CP_PROGRAM_ID)
|
||||
tweak {
|
||||
// The wrong pubkey.
|
||||
command(bigCorp.publicKey, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
`fails with`("output states are issued by a command signer")
|
||||
}
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Issue())
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
// DOCEND 7
|
||||
|
||||
// DOCSTART 8
|
||||
@Test(timeout=300_000)
|
||||
fun `chain commercial paper`() {
|
||||
val issuer = megaCorp.party.ref(123)
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy alice.party)
|
||||
}
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Issue())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party)
|
||||
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(alice.party))
|
||||
command(alice.publicKey, Cash.Commands.Move())
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
}
|
||||
// DOCEND 8
|
||||
|
||||
// DOCSTART 9
|
||||
@Test(timeout=300_000)
|
||||
fun `chain commercial paper double spend`() {
|
||||
val issuer = megaCorp.party.ref(123)
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy alice.party)
|
||||
}
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Issue())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party)
|
||||
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(alice.party))
|
||||
command(alice.publicKey, Cash.Commands.Move())
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("paper")
|
||||
// We moved a paper to another pubkey.
|
||||
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(bob.party))
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
|
||||
fails()
|
||||
}
|
||||
}
|
||||
// DOCEND 9
|
||||
|
||||
// DOCSTART 10
|
||||
@Test(timeout=300_000)
|
||||
fun `chain commercial tweak`() {
|
||||
val issuer = megaCorp.party.ref(123)
|
||||
ledgerServices.ledger(dummyNotary.party) {
|
||||
unverifiedTransaction {
|
||||
attachments(Cash.PROGRAM_ID)
|
||||
output(Cash.PROGRAM_ID, "alice's $900", 900.DOLLARS.CASH issuedBy issuer ownedBy alice.party)
|
||||
}
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output(CP_PROGRAM_ID, "paper", getPaper())
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Issue())
|
||||
attachments(CP_PROGRAM_ID)
|
||||
timeWindow(TEST_TX_TIME)
|
||||
verifies()
|
||||
}
|
||||
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output(Cash.PROGRAM_ID, "borrowed $900", 900.DOLLARS.CASH issuedBy issuer ownedBy megaCorp.party)
|
||||
output(CP_PROGRAM_ID, "alice's paper", "paper".output<ICommercialPaperState>().withOwner(alice.party))
|
||||
command(alice.publicKey, Cash.Commands.Move(CommercialPaper::class.java))
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
|
||||
tweak {
|
||||
transaction {
|
||||
input("paper")
|
||||
// We moved a paper to another pubkey.
|
||||
output(CP_PROGRAM_ID, "bob's paper", "paper".output<ICommercialPaperState>().withOwner(bob.party))
|
||||
command(megaCorp.publicKey, CommercialPaper.Commands.Move())
|
||||
verifies()
|
||||
}
|
||||
fails()
|
||||
}
|
||||
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
// DOCEND 10
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package net.corda.docs.kotlin.txbuild
|
||||
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName_
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WorkflowTransactionBuildTutorialTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var aliceNode: StartedMockNode
|
||||
private lateinit var bobNode: StartedMockNode
|
||||
private lateinit var alice: Party
|
||||
private lateinit var bob: Party
|
||||
|
||||
// Helper method to locate the latest Vault version of a LinearState
|
||||
private inline fun <reified T : LinearState> ServiceHub.latest(ref: UniqueIdentifier): StateAndRef<T> {
|
||||
val linearHeads = vaultService.queryBy<T>(QueryCriteria.LinearStateQueryCriteria(uuid = listOf(ref.id)))
|
||||
return linearHeads.states.single()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf(javaClass.packageName_))
|
||||
aliceNode = mockNet.createPartyNode(ALICE_NAME)
|
||||
bobNode = mockNet.createPartyNode(BOB_NAME)
|
||||
alice = aliceNode.services.myInfo.identityFromX500Name(ALICE_NAME)
|
||||
bob = bobNode.services.myInfo.identityFromX500Name(BOB_NAME)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `Run workflow to completion`() {
|
||||
// Setup a vault subscriber to wait for successful upload of the proposal to NodeB
|
||||
val nodeBVaultUpdate = bobNode.services.vaultService.updates.toFuture()
|
||||
// Kick of the proposal flow
|
||||
val flow1 = aliceNode.startFlow(SubmitTradeApprovalFlow("1234", bob, mockNet.defaultNotaryIdentity))
|
||||
// Wait for the flow to finish
|
||||
val proposalRef = flow1.getOrThrow()
|
||||
val proposalLinearId = proposalRef.state.data.linearId
|
||||
// Wait for NodeB to include it's copy in the vault
|
||||
nodeBVaultUpdate.get()
|
||||
// Fetch the latest copy of the state from both nodes
|
||||
val latestFromA = aliceNode.transaction {
|
||||
aliceNode.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
val latestFromB = bobNode.transaction {
|
||||
bobNode.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
// Confirm the state as as expected
|
||||
assertEquals(WorkflowState.NEW, proposalRef.state.data.state)
|
||||
assertEquals("1234", proposalRef.state.data.tradeId)
|
||||
assertEquals(alice, proposalRef.state.data.source)
|
||||
assertEquals(bob, proposalRef.state.data.counterparty)
|
||||
assertEquals(proposalRef, latestFromA)
|
||||
assertEquals(proposalRef, latestFromB)
|
||||
// Setup a vault subscriber to pause until the final update is in NodeA and NodeB
|
||||
val nodeAVaultUpdate = aliceNode.services.vaultService.updates.toFuture()
|
||||
val secondNodeBVaultUpdate = bobNode.services.vaultService.updates.toFuture()
|
||||
// Run the manual completion flow from NodeB
|
||||
val flow2 = bobNode.startFlow(SubmitCompletionFlow(latestFromB.ref, WorkflowState.APPROVED))
|
||||
// wait for the flow to end
|
||||
val completedRef = flow2.getOrThrow()
|
||||
// wait for the vault updates to stabilise
|
||||
nodeAVaultUpdate.get()
|
||||
secondNodeBVaultUpdate.get()
|
||||
// Fetch the latest copies from the vault
|
||||
val finalFromA = aliceNode.transaction {
|
||||
aliceNode.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
val finalFromB = bobNode.transaction {
|
||||
bobNode.services.latest<TradeApprovalContract.State>(proposalLinearId)
|
||||
}
|
||||
// Confirm the state is as expected
|
||||
assertEquals(WorkflowState.APPROVED, completedRef.state.data.state)
|
||||
assertEquals("1234", completedRef.state.data.tradeId)
|
||||
assertEquals(alice, completedRef.state.data.source)
|
||||
assertEquals(bob, completedRef.state.data.counterparty)
|
||||
assertEquals(completedRef, finalFromA)
|
||||
assertEquals(completedRef, finalFromB)
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
package net.corda.docs.kotlin.vault
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.internal.packageName_
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.docs.kotlin.tutorial.helloworld.IOUFlow
|
||||
import net.corda.finance.*
|
||||
import net.corda.finance.workflows.getCashBalances
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.services.vault.VaultSchemaV1
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.assertj.core.api.Assertions.assertThatCode
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
|
||||
class CustomVaultQueryTest {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var nodeA: StartedMockNode
|
||||
private lateinit var nodeB: StartedMockNode
|
||||
private lateinit var notary: Party
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(threadPerNode = true, cordappPackages = listOf("net.corda.finance", IOUFlow::class.packageName, javaClass.packageName_, "com.template"))
|
||||
nodeA = mockNet.createPartyNode()
|
||||
nodeB = mockNet.createPartyNode()
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `query by max recorded time`() {
|
||||
nodeA.startFlow(IOUFlow(1000, nodeB.info.singleIdentity())).getOrThrow()
|
||||
nodeA.startFlow(IOUFlow(500, nodeB.info.singleIdentity())).getOrThrow()
|
||||
|
||||
val max = builder { VaultSchemaV1.VaultStates::recordedTime.max() }
|
||||
val maxCriteria = QueryCriteria.VaultCustomQueryCriteria(max)
|
||||
|
||||
val results = nodeA.transaction {
|
||||
val pageSpecification = PageSpecification(pageNumber = DEFAULT_PAGE_NUM, pageSize = DEFAULT_PAGE_SIZE)
|
||||
nodeA.services.vaultService.queryBy<ContractState>(criteria = maxCriteria, paging = pageSpecification)
|
||||
}
|
||||
assertThatCode { results.otherResults.single() }.doesNotThrowAnyException()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `test custom vault query`() {
|
||||
// issue some cash in several currencies
|
||||
issueCashForCurrency(POUNDS(1000))
|
||||
issueCashForCurrency(DOLLARS(900))
|
||||
issueCashForCurrency(SWISS_FRANCS(800))
|
||||
val (cashBalancesOriginal, _) = getBalances()
|
||||
|
||||
// top up all currencies (by double original)
|
||||
topUpCurrencies()
|
||||
val (cashBalancesAfterTopup, _) = getBalances()
|
||||
|
||||
assertEquals(cashBalancesOriginal[GBP]?.times(2), cashBalancesAfterTopup[GBP])
|
||||
assertEquals(cashBalancesOriginal[USD]?.times(2) , cashBalancesAfterTopup[USD])
|
||||
assertEquals(cashBalancesOriginal[CHF]?.times( 2), cashBalancesAfterTopup[CHF])
|
||||
}
|
||||
|
||||
private fun issueCashForCurrency(amountToIssue: Amount<Currency>) {
|
||||
// Use NodeA as issuer and create some dollars
|
||||
nodeA.startFlow(CashIssueFlow(amountToIssue,
|
||||
OpaqueBytes.of(0x01),
|
||||
notary)).getOrThrow()
|
||||
}
|
||||
|
||||
private fun topUpCurrencies() {
|
||||
nodeA.startFlow(TopupIssuerFlow.TopupIssuanceRequester(
|
||||
nodeA.info.singleIdentity(),
|
||||
OpaqueBytes.of(0x01),
|
||||
nodeA.info.singleIdentity(),
|
||||
notary)
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
private fun getBalances(): Pair<Map<Currency, Amount<Currency>>, Map<Currency, Amount<Currency>>> {
|
||||
// Print out the balances
|
||||
val balancesNodesA = nodeA.transaction {
|
||||
nodeA.services.getCashBalances()
|
||||
}
|
||||
println("BalanceA\n$balancesNodesA")
|
||||
|
||||
val balancesNodesB = nodeB.transaction {
|
||||
nodeB.services.getCashBalances()
|
||||
}
|
||||
println("BalanceB\n$balancesNodesB")
|
||||
|
||||
return Pair(balancesNodesA, balancesNodesB)
|
||||
}
|
||||
}
|
@ -122,6 +122,19 @@ abstract class StatemachineErrorHandlingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class GetNumberOfHospitalizedCheckpointsFlow : FlowLogic<Long>() {
|
||||
override fun call(): Long {
|
||||
val sqlStatement = "select count(*) from node_checkpoints where status in (${Checkpoint.FlowStatus.HOSPITALIZED.ordinal})"
|
||||
return serviceHub.jdbcSession().prepareStatement(sqlStatement).use { ps ->
|
||||
ps.executeQuery().use { rs ->
|
||||
rs.next()
|
||||
rs.getLong(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal use for testing only!!
|
||||
@StartableByRPC
|
||||
class GetHospitalCountersFlow : FlowLogic<HospitalCounts>() {
|
||||
|
@ -1178,9 +1178,9 @@ class StatemachineGeneralErrorHandlingTest : StatemachineErrorHandlingTest() {
|
||||
// 1 for the flow that is waiting for the errored counterparty flow to finish and 1 for GetNumberOfCheckpointsFlow
|
||||
assertEquals(2, aliceClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfUncompletedCheckpointsFlow).returnValue.get())
|
||||
// 1 for GetNumberOfCheckpointsFlow
|
||||
// the checkpoint is not persisted since it kept failing the original checkpoint commit
|
||||
// a hospitalized flow is saved as the original checkpoint kept failing to commit
|
||||
// the flow will recover since artemis will keep the events and replay them on node restart
|
||||
assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfUncompletedCheckpointsFlow).returnValue.get())
|
||||
assertEquals(1, charlieClient.startFlow(StatemachineErrorHandlingTest::GetNumberOfHospitalizedCheckpointsFlow).returnValue.get())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,8 +339,7 @@ open class NodeStartup : NodeStartupLogging {
|
||||
if (devMode) return true
|
||||
|
||||
if (!certDirectory.isDirectory()) {
|
||||
printError("Unable to access certificates directory ${certDirectory}. This could be because the node has not been registered with the Identity Operator.")
|
||||
printError("Node will now shutdown.")
|
||||
logger.error("Unable to access certificates directory ${certDirectory}. This could be because the node has not been registered with the Identity Operator. Node will now shutdown")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -510,6 +509,7 @@ interface NodeStartupLogging {
|
||||
|
||||
fun CliWrapperBase.initLogging(baseDirectory: Path): Boolean {
|
||||
System.setProperty("defaultLogLevel", specifiedLogLevel) // These properties are referenced from the XML config file.
|
||||
System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString())
|
||||
if (verbose) {
|
||||
System.setProperty("consoleLoggingEnabled", "true")
|
||||
System.setProperty("consoleLogLevel", specifiedLogLevel)
|
||||
@ -532,7 +532,6 @@ fun CliWrapperBase.initLogging(baseDirectory: Path): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString())
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
|
||||
SLF4JBridgeHandler.install()
|
||||
return true
|
||||
|
@ -5,6 +5,7 @@ import io.github.classgraph.ClassInfo
|
||||
import io.github.classgraph.ScanResult
|
||||
import net.corda.common.logging.errorReporting.CordappErrors
|
||||
import net.corda.common.logging.errorReporting.ErrorCode
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
@ -105,12 +106,15 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
}
|
||||
|
||||
private fun loadCordapps(): List<CordappImpl> {
|
||||
val invalidCordapps = mutableMapOf<String, URL>()
|
||||
|
||||
val cordapps = cordappJarPaths
|
||||
.map { url -> scanCordapp(url).use { it.toCordapp(url) } }
|
||||
.filter {
|
||||
if (it.minimumPlatformVersion > versionInfo.platformVersion) {
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum " +
|
||||
"platform version ${it.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
|
||||
invalidCordapps.put("CorDapp requires minimumPlatformVersion: ${it.minimumPlatformVersion}, but was: ${versionInfo.platformVersion}", it.jarPath)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@ -125,12 +129,19 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
if (certificates.isEmpty() || (certificates - blockedCertificates).isNotEmpty())
|
||||
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
|
||||
else {
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " +
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by blacklisted key(s) only (probably development key): " +
|
||||
"${blockedCertificates.map { it.publicKey }}.")
|
||||
invalidCordapps.put("Corresponding contracts are signed by blacklisted key(s) only (probably development key),", it.jarPath)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidCordapps.isNotEmpty()) {
|
||||
throw InvalidCordappException("Invalid Cordapps found, that couldn't be loaded: " +
|
||||
"${invalidCordapps.map { "Problem: ${it.key} in Cordapp ${it.value}" }}, ")
|
||||
}
|
||||
|
||||
cordapps.forEach(::register)
|
||||
return cordapps
|
||||
}
|
||||
@ -447,7 +458,7 @@ class MultipleCordappsForFlowException(
|
||||
message: String,
|
||||
flowName: String,
|
||||
jars: String
|
||||
) : Exception(message), ErrorCode<CordappErrors> {
|
||||
) : CordaRuntimeException(message), ErrorCode<CordappErrors> {
|
||||
override val code = CordappErrors.MULTIPLE_CORDAPPS_FOR_FLOW
|
||||
override val parameters = listOf(flowName, jars)
|
||||
}
|
||||
@ -459,19 +470,24 @@ class CordappInvalidVersionException(
|
||||
msg: String,
|
||||
override val code: CordappErrors,
|
||||
override val parameters: List<Any> = listOf()
|
||||
) : Exception(msg), ErrorCode<CordappErrors>
|
||||
) : CordaRuntimeException(msg), ErrorCode<CordappErrors>
|
||||
|
||||
/**
|
||||
* Thrown if duplicate CorDapps are installed on the node
|
||||
*/
|
||||
class DuplicateCordappsInstalledException(app: Cordapp, duplicates: Set<Cordapp>)
|
||||
: IllegalStateException("The CorDapp (name: ${app.info.shortName}, file: ${app.name}) " +
|
||||
: CordaRuntimeException("IllegalStateExcepion", "The CorDapp (name: ${app.info.shortName}, file: ${app.name}) " +
|
||||
"is installed multiple times on the node. The following files correspond to the exact same content: " +
|
||||
"${duplicates.map { it.name }}"), ErrorCode<CordappErrors> {
|
||||
"${duplicates.map { it.name }}", null), ErrorCode<CordappErrors> {
|
||||
override val code = CordappErrors.DUPLICATE_CORDAPPS_INSTALLED
|
||||
override val parameters = listOf(app.info.shortName, app.name, duplicates.map { it.name })
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown if an exception occurs during loading cordapps.
|
||||
*/
|
||||
class InvalidCordappException(message: String) : CordaRuntimeException(message)
|
||||
|
||||
abstract class CordappLoaderTemplate : CordappLoader {
|
||||
|
||||
companion object {
|
||||
|
@ -9,16 +9,13 @@ import liquibase.statement.core.UpdateStatement
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
|
||||
class PersistentIdentityMigration : CustomSqlChange {
|
||||
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
const val PUB_KEY_HASH_TO_PARTY_AND_CERT_TABLE = PersistentIdentityService.HASH_TO_IDENTITY_TABLE_NAME
|
||||
const val X500_NAME_TO_PUB_KEY_HASH_TABLE = PersistentIdentityService.NAME_TO_HASH_TABLE_NAME
|
||||
const val PUB_KEY_HASH_TO_PARTY_AND_CERT_TABLE = "node_identities"
|
||||
const val X500_NAME_TO_PUB_KEY_HASH_TABLE = "node_named_identities"
|
||||
}
|
||||
|
||||
override fun validate(database: Database?): ValidationErrors? {
|
||||
|
@ -1,28 +1,27 @@
|
||||
package net.corda.node.migration
|
||||
|
||||
import liquibase.change.custom.CustomTaskChange
|
||||
import liquibase.database.Database
|
||||
import liquibase.database.jvm.JdbcConnection
|
||||
import liquibase.exception.ValidationErrors
|
||||
import liquibase.resource.ResourceAccessor
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.internal.DBNetworkParametersStorage
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* Migration that reads data from the [PersistentIdentityCert] table, extracts the parameters required to insert into the [PersistentIdentity] table.
|
||||
*/
|
||||
class PersistentIdentityMigrationNewTable : CordaMigration() {
|
||||
class PersistentIdentityMigrationNewTable : CustomTaskChange {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
private lateinit var ourName: CordaX500Name
|
||||
|
||||
override fun execute(database: Database?) {
|
||||
logger.info("Migrating persistent identities with certificates table into persistent table with no certificate data.")
|
||||
|
||||
@ -30,7 +29,7 @@ class PersistentIdentityMigrationNewTable : CordaMigration() {
|
||||
logger.error("Cannot migrate persistent identities: Liquibase failed to provide a suitable database connection")
|
||||
throw PersistentIdentitiesMigrationException("Cannot migrate persistent identities as liquibase failed to provide a suitable database connection")
|
||||
}
|
||||
initialiseNodeServices(database, setOf(PersistentIdentitiesMigrationSchemaBuilder.getMappedSchema()))
|
||||
ourName = CordaX500Name.parse(System.getProperty(SchemaMigration.NODE_X500_NAME))
|
||||
|
||||
val connection = database.connection as JdbcConnection
|
||||
val hashToKeyAndName = extractKeyAndName(connection)
|
||||
@ -68,7 +67,7 @@ class PersistentIdentityMigrationNewTable : CordaMigration() {
|
||||
it.setString(2, name.toString())
|
||||
it.executeUpdate()
|
||||
}
|
||||
if (name !in identityService.ourNames) {
|
||||
if (name != ourName) {
|
||||
connection.prepareStatement("INSERT INTO node_hash_to_key (pk_hash, public_key) VALUES (?,?)").use {
|
||||
it.setString(1, publicKeyHash)
|
||||
it.setBytes(2, publicKey.encoded)
|
||||
@ -92,26 +91,4 @@ class PersistentIdentityMigrationNewTable : CordaMigration() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal set of schema for retrieving data from the database.
|
||||
*
|
||||
* Note that adding an extra schema here may cause migrations to fail if it ends up creating a table before the same table
|
||||
* is created in a migration script. As such, this migration must be run after the tables for the following have been created (and,
|
||||
* if they are removed in the future, before they are deleted).
|
||||
*/
|
||||
object PersistentIdentitiesMigrationSchema
|
||||
|
||||
object PersistentIdentitiesMigrationSchemaBuilder {
|
||||
fun getMappedSchema() =
|
||||
MappedSchema(schemaFamily = PersistentIdentitiesMigrationSchema.javaClass, version = 1,
|
||||
mappedTypes = listOf(
|
||||
DBTransactionStorage.DBTransaction::class.java,
|
||||
PersistentIdentityService.PersistentPublicKeyHashToCertificate::class.java,
|
||||
PersistentIdentityService.PersistentPartyToPublicKeyHash::class.java,
|
||||
PersistentIdentityService.PersistentPublicKeyHashToParty::class.java,
|
||||
PersistentIdentityService.PersistentHashToPublicKey::class.java,
|
||||
NodeAttachmentService.DBAttachment::class.java,
|
||||
DBNetworkParametersStorage.PersistentNetworkParameters::class.java
|
||||
))
|
||||
}
|
||||
class PersistentIdentitiesMigrationException(msg: String, cause: Exception? = null) : Exception(msg, cause)
|
@ -77,7 +77,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
|
||||
val messagingServerExternal = config[messagingServerExternal] ?: Defaults.messagingServerExternal(config[messagingServerAddress])
|
||||
val database = config[database] ?: Defaults.database(config[devMode])
|
||||
val baseDirectoryPath = config[baseDirectory]
|
||||
val cordappDirectories = config[cordappDirectories] ?: Defaults.cordappsDirectories(baseDirectoryPath)
|
||||
val cordappDirectories = config[cordappDirectories]?.map { baseDirectoryPath.resolve(it) } ?: Defaults.cordappsDirectories(baseDirectoryPath)
|
||||
val result = try {
|
||||
valid<NodeConfigurationImpl, Configuration.Validation.Error>(NodeConfigurationImpl(
|
||||
baseDirectory = baseDirectoryPath,
|
||||
@ -127,7 +127,7 @@ internal object V1NodeConfigurationSpec : Configuration.Specification<NodeConfig
|
||||
attachmentContentCacheSizeMegaBytes = config[attachmentContentCacheSizeMegaBytes],
|
||||
h2port = config[h2port],
|
||||
jarDirs = config[jarDirs],
|
||||
cordappDirectories = cordappDirectories.map { baseDirectoryPath.resolve(it) },
|
||||
cordappDirectories = cordappDirectories,
|
||||
cordappSignerKeyFingerprintBlacklist = config[cordappSignerKeyFingerprintBlacklist],
|
||||
blacklistedAttachmentSigningKeys = config[blacklistedAttachmentSigningKeys],
|
||||
networkParameterAcceptanceSettings = config[networkParameterAcceptanceSettings],
|
||||
|
@ -24,13 +24,12 @@
|
||||
<include file="migration/node-core.changelog-v13.xml"/>
|
||||
<!-- This change should be done before the v14-data migration. -->
|
||||
<include file="migration/node-core.changelog-v15.xml"/>
|
||||
<include file="migration/node-core.changelog-v14-data.xml"/>
|
||||
<include file="migration/node-core.changelog-v16.xml"/>
|
||||
|
||||
<!-- This must run after node-core.changelog-init.xml, to prevent database columns being created twice. -->
|
||||
<include file="migration/vault-schema.changelog-v9.xml"/>
|
||||
|
||||
<include file="migration/node-core.changelog-v14-data.xml"/>
|
||||
|
||||
<include file="migration/node-core.changelog-v19.xml"/>
|
||||
<include file="migration/node-core.changelog-v19-postgres.xml"/>
|
||||
<include file="migration/node-core.changelog-v19-keys.xml"/>
|
||||
|
@ -2,14 +2,19 @@ package net.corda.node.internal
|
||||
|
||||
import net.corda.cliutils.CommonCliConstants
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.slf4j.event.Level
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NodeStartupCliTest {
|
||||
private val startup = NodeStartupCli()
|
||||
@ -49,4 +54,17 @@ class NodeStartupCliTest {
|
||||
Assertions.assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf")
|
||||
Assertions.assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
|
||||
}
|
||||
|
||||
@Test(timeout=3_000)
|
||||
fun `test logs are written to correct location correctly if verbose flag set`() {
|
||||
val node = NodeStartupCli()
|
||||
val dir = Files.createTempDirectory("verboseLoggingTest")
|
||||
node.verbose = true
|
||||
// With verbose set, initLogging can accidentally attempt to access a logger before all required system properties are set. This
|
||||
// causes the logging config to be parsed too early, resulting in logs being written to the wrong directory
|
||||
node.initLogging(dir)
|
||||
LoggerFactory.getLogger("").debug("Test message")
|
||||
assertTrue(dir.resolve("logs").exists())
|
||||
assertFalse(Paths.get("./logs").exists())
|
||||
}
|
||||
}
|
@ -203,7 +203,7 @@ class CordappProviderImplTests {
|
||||
Files.copy(signedJarPath, duplicateJarPath)
|
||||
val urls = asList(signedJarPath.toUri().toURL(), duplicateJarPath.toUri().toURL())
|
||||
JarScanningCordappLoader.fromJarUrls(urls, VersionInfo.UNKNOWN).use {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
assertFailsWith<DuplicateCordappsInstalledException> {
|
||||
CordappProviderImpl(it, stubConfigProvider, attachmentStore).apply { start() }
|
||||
}
|
||||
}
|
||||
|
@ -137,11 +137,10 @@ class JarScanningCordappLoaderTest {
|
||||
assertThat(cordapp.minimumPlatformVersion).isEqualTo(2)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(expected = InvalidCordappException::class, timeout = 300_000)
|
||||
fun `cordapp classloader does not load apps when their min platform version is greater than the node platform version`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("versions/min-2-no-target.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1))
|
||||
assertThat(loader.cordapps).hasSize(0)
|
||||
JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 1)).cordapps
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@ -165,11 +164,10 @@ class JarScanningCordappLoaderTest {
|
||||
assertThat(loader.cordapps).hasSize(1)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(expected = InvalidCordappException::class, timeout = 300_000)
|
||||
fun `cordapp classloader does not load app signed by blacklisted certificate`() {
|
||||
val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!!
|
||||
val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES)
|
||||
assertThat(loader.cordapps).hasSize(0)
|
||||
JarScanningCordappLoader.fromJarUrls(listOf(jar), cordappsSignerKeyFingerprintBlacklist = DEV_PUB_KEY_HASHES).cordapps
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.node.migration
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import liquibase.database.core.H2Database
|
||||
import liquibase.database.jvm.JdbcConnection
|
||||
import net.corda.core.crypto.toStringShort
|
||||
@ -7,7 +9,8 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.coretesting.internal.rigorousMock
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.contextTransactionOrNull
|
||||
@ -44,11 +47,16 @@ class IdentityServiceToStringShortMigrationTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val schemaService = rigorousMock<SchemaService>()
|
||||
doReturn(setOf(IdentityTestSchemaV1)).whenever(schemaService).schemas
|
||||
|
||||
cordaDB = configureDatabase(
|
||||
makeTestDataSourceProperties(),
|
||||
DatabaseConfig(),
|
||||
{ null },
|
||||
{ null },
|
||||
schemaService = schemaService,
|
||||
internalSchemas = setOf(),
|
||||
ourName = BOB_IDENTITY.name)
|
||||
liquibaseDB = H2Database()
|
||||
liquibaseDB.connection = JdbcConnection(cordaDB.dataSource.connection)
|
||||
@ -66,8 +74,8 @@ class IdentityServiceToStringShortMigrationTest {
|
||||
cordaDB.transaction {
|
||||
val groupedIdentities = identities.groupBy { it.name }
|
||||
groupedIdentities.forEach { name, certs ->
|
||||
val persistentIDs = certs.map { PersistentIdentityService.PersistentPublicKeyHashToCertificate(it.owningKey.hash.toString(), it.certPath.encoded) }
|
||||
val persistentName = PersistentIdentityService.PersistentPartyToPublicKeyHash(name.toString(), certs.first().owningKey.hash.toString())
|
||||
val persistentIDs = certs.map { IdentityTestSchemaV1.NodeIdentities(it.owningKey.hash.toString(), it.certPath.encoded) }
|
||||
val persistentName = IdentityTestSchemaV1.NodeNamedIdentities(name.toString(), certs.first().owningKey.hash.toString())
|
||||
persistentIDs.forEach {
|
||||
session.persist(it)
|
||||
}
|
||||
@ -87,7 +95,7 @@ class IdentityServiceToStringShortMigrationTest {
|
||||
identities.forEach {
|
||||
logger.info("Checking: ${it.name}")
|
||||
cordaDB.transaction {
|
||||
val hashToIdentityStatement = database.dataSource.connection.prepareStatement("SELECT ${PersistentIdentityService.PK_HASH_COLUMN_NAME} FROM ${PersistentIdentityService.HASH_TO_IDENTITY_TABLE_NAME} WHERE pk_hash=?")
|
||||
val hashToIdentityStatement = database.dataSource.connection.prepareStatement("SELECT pk_hash FROM node_identities WHERE pk_hash=?")
|
||||
hashToIdentityStatement.setString(1, it.owningKey.toStringShort())
|
||||
val hashToIdentityResultSet = hashToIdentityStatement.executeQuery()
|
||||
|
||||
@ -96,7 +104,7 @@ class IdentityServiceToStringShortMigrationTest {
|
||||
//check that the pk_hash actually matches what we expect (kinda redundant, but deserializing the whole PartyAndCertificate feels like overkill)
|
||||
Assert.assertThat(hashToIdentityResultSet.getString(1), `is`(it.owningKey.toStringShort()))
|
||||
|
||||
val nameToHashStatement = connection.prepareStatement("SELECT ${PersistentIdentityService.NAME_COLUMN_NAME} FROM ${PersistentIdentityService.NAME_TO_HASH_TABLE_NAME} WHERE pk_hash=?")
|
||||
val nameToHashStatement = connection.prepareStatement("SELECT name FROM node_named_identities WHERE pk_hash=?")
|
||||
nameToHashStatement.setString(1, it.owningKey.toStringShort())
|
||||
val nameToHashResultSet = nameToHashStatement.executeQuery()
|
||||
|
||||
|
@ -0,0 +1,78 @@
|
||||
package net.corda.node.migration
|
||||
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.hibernate.annotations.Type
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.Table
|
||||
|
||||
object MigrationTestSchema
|
||||
|
||||
/**
|
||||
* Schema definition for testing PersistentIdentityService custom migration scripts at the moment when scripts were written.
|
||||
* Used to break dependency on the latest PersistentIdentityService which schema version may be different.
|
||||
*
|
||||
* This will allow:
|
||||
* - to fix the position of relevant scripts in the node-core.changelog-master.xml (instead of placing them at the end)
|
||||
* - to perform further modifications of PersistentIdentityService schema without impacting existing migration scripts and their tests
|
||||
*/
|
||||
object IdentityTestSchemaV1 : MappedSchema(
|
||||
schemaFamily = MigrationTestSchema::class.java,
|
||||
version = 1,
|
||||
mappedTypes = listOf(
|
||||
NodeIdentities::class.java,
|
||||
NodeNamedIdentities::class.java,
|
||||
NodeIdentitiesNoCert::class.java,
|
||||
NodeHashToKey::class.java
|
||||
)
|
||||
) {
|
||||
@Entity
|
||||
@Table(name = "node_identities")
|
||||
class NodeIdentities(
|
||||
@Id
|
||||
@Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = false)
|
||||
var publicKeyHash: String = "",
|
||||
|
||||
@Type(type = "corda-blob")
|
||||
@Column(name = "identity_value", nullable = false)
|
||||
var identity: ByteArray = ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Table(name = "node_named_identities")
|
||||
class NodeNamedIdentities(
|
||||
@Id
|
||||
@Suppress("MagicNumber") // database column width
|
||||
@Column(name = "name", length = 128, nullable = false)
|
||||
var name: String = "",
|
||||
|
||||
@Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = false)
|
||||
var publicKeyHash: String = ""
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Table(name = "node_identities_no_cert")
|
||||
class NodeIdentitiesNoCert(
|
||||
@Id
|
||||
@Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = false)
|
||||
var publicKeyHash: String = "",
|
||||
|
||||
@Column(name = "name", length = 128, nullable = false)
|
||||
var name: String = ""
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Table(name = "node_hash_to_key")
|
||||
class NodeHashToKey(
|
||||
@Id
|
||||
@Column(name = "pk_hash", length = MAX_HASH_HEX_SIZE, nullable = false)
|
||||
var publicKeyHash: String = "",
|
||||
|
||||
@Type(type = "corda-blob")
|
||||
@Column(name = "public_key", nullable = false)
|
||||
var publicKey: ByteArray = ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
)
|
||||
}
|
@ -1,48 +1,35 @@
|
||||
package net.corda.node.migration
|
||||
|
||||
import liquibase.database.Database
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import liquibase.database.core.H2Database
|
||||
import liquibase.database.jvm.JdbcConnection
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.internal.signWithCert
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.node.internal.DBNetworkParametersStorage
|
||||
import net.corda.node.migration.VaultStateMigrationTest.Companion.CHARLIE
|
||||
import net.corda.node.migration.VaultStateMigrationTest.Companion.DUMMY_NOTARY
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.node.services.keys.BasicHSMKeyManagementService
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.coretesting.internal.rigorousMock
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.nodeapi.internal.persistence.SchemaMigration
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestIdentityService
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito
|
||||
import java.security.KeyPair
|
||||
import java.sql.Connection
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
class PersistentIdentityMigrationNewTableTest{
|
||||
class PersistentIdentityMigrationNewTableTest {
|
||||
companion object {
|
||||
val alice = TestIdentity(ALICE_NAME, 70)
|
||||
val bankOfCorda = TestIdentity(BOC_NAME)
|
||||
val bob = TestIdentity(BOB_NAME, 80)
|
||||
val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10)
|
||||
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
|
||||
val ALICE_IDENTITY get() = alice.identity
|
||||
val BOB_IDENTITY get() = bob.identity
|
||||
val BOC_IDENTITY get() = bankOfCorda.identity
|
||||
val BOC_KEY get() = bankOfCorda.keyPair
|
||||
val bob2 = TestIdentity(BOB_NAME, 40)
|
||||
val BOB2_IDENTITY = bob2.identity
|
||||
|
||||
@ -51,122 +38,62 @@ class PersistentIdentityMigrationNewTableTest{
|
||||
val testSerialization = SerializationEnvironmentRule()
|
||||
}
|
||||
|
||||
lateinit var liquidBaseDB: Database
|
||||
lateinit var liquibaseDB: H2Database
|
||||
lateinit var cordaDB: CordaPersistence
|
||||
lateinit var notaryServices: MockServices
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val identityService = makeTestIdentityService(PersistentIdentityMigrationNewTableTest.dummyNotary.identity, BOB_IDENTITY, ALICE_IDENTITY)
|
||||
notaryServices = MockServices(listOf("net.corda.finance.contracts"), dummyNotary, identityService, dummyCashIssuer.keyPair, BOC_KEY)
|
||||
val schemaService = rigorousMock<SchemaService>()
|
||||
doReturn(setOf(IdentityTestSchemaV1)).whenever(schemaService).schemas
|
||||
System.setProperty(SchemaMigration.NODE_X500_NAME, BOB_IDENTITY.name.toString())
|
||||
|
||||
// Runs migration tasks
|
||||
cordaDB = configureDatabase(
|
||||
MockServices.makeTestDataSourceProperties(),
|
||||
DatabaseConfig(),
|
||||
notaryServices.identityService::wellKnownPartyFromX500Name,
|
||||
notaryServices.identityService::wellKnownPartyFromAnonymous,
|
||||
{ null },
|
||||
{ null },
|
||||
schemaService = schemaService,
|
||||
internalSchemas = setOf(),
|
||||
ourName = BOB_IDENTITY.name)
|
||||
val liquidbaseConnection = Mockito.mock(JdbcConnection::class.java)
|
||||
Mockito.`when`(liquidbaseConnection.url).thenReturn(cordaDB.jdbcUrl)
|
||||
Mockito.`when`(liquidbaseConnection.wrappedConnection).thenReturn(cordaDB.dataSource.connection)
|
||||
liquidBaseDB = Mockito.mock(Database::class.java)
|
||||
Mockito.`when`(liquidBaseDB.connection).thenReturn(liquidbaseConnection)
|
||||
|
||||
cordaDB.dataSource.connection
|
||||
saveOurKeys(listOf(bob.keyPair, bob2.keyPair))
|
||||
saveAllIdentities(listOf(BOB_IDENTITY, ALICE_IDENTITY, BOC_IDENTITY, dummyNotary.identity, BOB2_IDENTITY))
|
||||
addNetworkParameters()
|
||||
liquibaseDB = H2Database()
|
||||
liquibaseDB.connection = JdbcConnection(cordaDB.dataSource.connection)
|
||||
liquibaseDB.isAutoCommit = true
|
||||
}
|
||||
|
||||
@After
|
||||
fun `close`() {
|
||||
fun close() {
|
||||
cordaDB.close()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `migrate identities to new table`() {
|
||||
val pkHash = addTestMapping(cordaDB.dataSource.connection, alice)
|
||||
PersistentIdentityMigrationNewTable()
|
||||
verifyTestMigration(cordaDB.dataSource.connection, pkHash, alice.name.toString())
|
||||
@Test(timeout = 300_000)
|
||||
fun `migrate identities to new table`() {
|
||||
val identities = listOf(BOB_IDENTITY, ALICE_IDENTITY, BOC_IDENTITY, dummyNotary.identity, BOB2_IDENTITY)
|
||||
saveAllIdentities(identities)
|
||||
|
||||
PersistentIdentityMigrationNewTable().execute(liquibaseDB)
|
||||
|
||||
val expectedParties = identities.map { it.owningKey.toStringShort() to it.toString() }
|
||||
val actualParties = selectAll<IdentityTestSchemaV1.NodeIdentitiesNoCert>().map { it.publicKeyHash to it.name }
|
||||
assertThat(actualParties).isEqualTo(expectedParties)
|
||||
|
||||
val expectedKeys = listOf(ALICE_IDENTITY, BOC_IDENTITY, dummyNotary.identity).map { it.owningKey.toStringShort() to it.owningKey }
|
||||
val actualKeys = selectAll<IdentityTestSchemaV1.NodeHashToKey>().map { it.publicKeyHash to Crypto.decodePublicKey(it.publicKey) }
|
||||
assertThat(actualKeys).isEqualTo(expectedKeys)
|
||||
}
|
||||
|
||||
private fun saveAllIdentities(identities: List<PartyAndCertificate>) {
|
||||
cordaDB.transaction {
|
||||
identities.forEach {
|
||||
session.save(PersistentIdentityService.PersistentPublicKeyHashToCertificate(it.owningKey.hash.toString(), it.certPath.encoded))
|
||||
session.save(IdentityTestSchemaV1.NodeIdentities(it.owningKey.toStringShort(), it.certPath.encoded))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveOurKeys(keys: List<KeyPair>) {
|
||||
cordaDB.transaction {
|
||||
keys.forEach {
|
||||
val persistentKey = BasicHSMKeyManagementService.PersistentKey(it.public, it.private)
|
||||
session.save(persistentKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addNetworkParameters() {
|
||||
cordaDB.transaction {
|
||||
val clock = Clock.systemUTC()
|
||||
val params = NetworkParameters(
|
||||
1,
|
||||
listOf(NotaryInfo(DUMMY_NOTARY, false), NotaryInfo(CHARLIE, false)),
|
||||
1,
|
||||
1,
|
||||
clock.instant(),
|
||||
1,
|
||||
mapOf(),
|
||||
Duration.ZERO,
|
||||
mapOf()
|
||||
)
|
||||
val signedParams = params.signWithCert(bob.keyPair.private, BOB_IDENTITY.certificate)
|
||||
val persistentParams = DBNetworkParametersStorage.PersistentNetworkParameters(
|
||||
SecureHash.allOnesHash.toString(),
|
||||
params.epoch,
|
||||
signedParams.raw.bytes,
|
||||
signedParams.sig.bytes,
|
||||
signedParams.sig.by.encoded,
|
||||
X509Utilities.buildCertPath(signedParams.sig.parentCertsChain).encoded
|
||||
)
|
||||
session.save(persistentParams)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addTestMapping(connection: Connection, testIdentity: TestIdentity): String {
|
||||
val pkHash = UUID.randomUUID().toString()
|
||||
val cert = testIdentity.identity.certPath.encoded
|
||||
|
||||
connection.prepareStatement("INSERT INTO node_identities (pk_hash, identity_value) VALUES (?,?)").use {
|
||||
it.setString(1, pkHash)
|
||||
it.setBytes(2, cert)
|
||||
it.executeUpdate()
|
||||
}
|
||||
return pkHash
|
||||
}
|
||||
|
||||
// private fun deleteTestMapping(connection: Connection, pkHash: String) {
|
||||
// connection.prepareStatement("DELETE FROM node_identities WHERE pk_hash = ?").use {
|
||||
// it.setString(1, pkHash)
|
||||
// it.executeUpdate()
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun verifyTestMigration(connection: Connection, pk: String, name: String) {
|
||||
connection.createStatement().use {
|
||||
try {
|
||||
val rs = it.executeQuery("SELECT (pk_hash, name) FROM node_identities_no_cert")
|
||||
while (rs.next()) {
|
||||
val result = rs.getString(1)
|
||||
require(result.contains(pk))
|
||||
require(result.contains(name))
|
||||
}
|
||||
rs.close()
|
||||
} catch (e: Exception) {
|
||||
println(e.localizedMessage)
|
||||
}
|
||||
private inline fun <reified T> selectAll(): List<T> {
|
||||
return cordaDB.transaction {
|
||||
val criteria = session.criteriaBuilder.createQuery(T::class.java)
|
||||
criteria.select(criteria.from(T::class.java))
|
||||
session.createQuery(criteria).resultList
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import org.junit.Assert.assertNotNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.nio.file.Paths
|
||||
@ -34,7 +35,7 @@ class NodeConfigurationImplTest {
|
||||
@JvmField
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `can't have dev mode options if not in dev mode`() {
|
||||
val debugOptions = DevModeOptions()
|
||||
configDebugOptions(true, debugOptions)
|
||||
@ -43,7 +44,7 @@ class NodeConfigurationImplTest {
|
||||
configDebugOptions(false, null)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `can't have tlsCertCrlDistPoint null when tlsCertCrlIssuer is given`() {
|
||||
val configValidationResult = configTlsCertCrlOptions(null, "C=US, L=New York, OU=Corda, O=R3 HoldCo LLC, CN=Corda Root CA").validate()
|
||||
assertTrue { configValidationResult.isNotEmpty() }
|
||||
@ -51,7 +52,7 @@ class NodeConfigurationImplTest {
|
||||
assertThat(configValidationResult.first()).contains("tlsCertCrlIssuer")
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `can't have tlsCertCrlDistPoint null when crlCheckSoftFail is false`() {
|
||||
val configValidationResult = configTlsCertCrlOptions(null, null, false).validate()
|
||||
assertTrue { configValidationResult.isNotEmpty() }
|
||||
@ -59,7 +60,7 @@ class NodeConfigurationImplTest {
|
||||
assertThat(configValidationResult.first()).contains("crlCheckSoftFail")
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `check devModeOptions flag helper`() {
|
||||
assertTrue { configDebugOptions(true, null).shouldCheckCheckpoints() }
|
||||
assertTrue { configDebugOptions(true, DevModeOptions()).shouldCheckCheckpoints() }
|
||||
@ -67,7 +68,7 @@ class NodeConfigurationImplTest {
|
||||
assertFalse { configDebugOptions(true, DevModeOptions(true)).shouldCheckCheckpoints() }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `check crashShell flags helper`() {
|
||||
assertFalse { testConfiguration.copy(sshd = null).shouldStartSSHDaemon() }
|
||||
assertTrue { testConfiguration.copy(sshd = SSHDConfiguration(1234)).shouldStartSSHDaemon() }
|
||||
@ -79,7 +80,7 @@ class NodeConfigurationImplTest {
|
||||
assertFalse { testConfiguration.copy(noLocalShell = true, sshd = null).shouldInitCrashShell() }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `Dev mode is autodetected correctly`() {
|
||||
val os = System.getProperty("os.name")
|
||||
|
||||
@ -102,20 +103,20 @@ class NodeConfigurationImplTest {
|
||||
System.setProperty("os.name", os)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `Dev mode is read from the config over the autodetect logic`() {
|
||||
assertTrue(getConfig("test-config-DevMode.conf").getBooleanCaseInsensitive("devMode"))
|
||||
assertFalse(getConfig("test-config-noDevMode.conf").getBooleanCaseInsensitive("devMode"))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `Dev mode is true if overriden`() {
|
||||
assertTrue(getConfig("test-config-DevMode.conf", ConfigFactory.parseMap(mapOf("devMode" to true))).getBooleanCaseInsensitive("devMode"))
|
||||
assertTrue(getConfig("test-config-noDevMode.conf", ConfigFactory.parseMap(mapOf("devMode" to true))).getBooleanCaseInsensitive("devMode"))
|
||||
assertTrue(getConfig("test-config-empty.conf", ConfigFactory.parseMap(mapOf("devMode" to true))).getBooleanCaseInsensitive("devMode"))
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `Dev mode is false if overriden`() {
|
||||
assertFalse(getConfig("test-config-DevMode.conf", ConfigFactory.parseMap(mapOf("devMode" to false))).getBooleanCaseInsensitive("devMode"))
|
||||
assertFalse(getConfig("test-config-noDevMode.conf", ConfigFactory.parseMap(mapOf("devMode" to false))).getBooleanCaseInsensitive("devMode"))
|
||||
@ -131,7 +132,7 @@ class NodeConfigurationImplTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `validation has error when compatibilityZoneURL is present and devMode is true`() {
|
||||
val configuration = testConfiguration.copy(
|
||||
devMode = true,
|
||||
@ -142,7 +143,7 @@ class NodeConfigurationImplTest {
|
||||
assertThat(errors).hasOnlyOneElementSatisfying { error -> error.contains("compatibilityZoneURL") && error.contains("devMode") }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `validation succeeds when compatibilityZoneURL is present and devMode is true and allowCompatibilityZoneURL is set`() {
|
||||
val configuration = testConfiguration.copy(
|
||||
devMode = true,
|
||||
@ -153,7 +154,7 @@ class NodeConfigurationImplTest {
|
||||
assertThat(errors).isEmpty()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `errors for nested config keys contain path`() {
|
||||
var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
val missingPropertyPath = "rpcSettings.address"
|
||||
@ -165,7 +166,7 @@ class NodeConfigurationImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `validation has error when compatibilityZone is present and devMode is true`() {
|
||||
val configuration = testConfiguration.copy(devMode = true, networkServices = NetworkServicesConfig(
|
||||
URL("https://r3.com.doorman"),
|
||||
@ -176,7 +177,7 @@ class NodeConfigurationImplTest {
|
||||
assertThat(errors).hasOnlyOneElementSatisfying { error -> error.contains("networkServices") && error.contains("devMode") }
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `validation has error when both compatibilityZoneURL and networkServices are configured`() {
|
||||
val configuration = testConfiguration.copy(
|
||||
devMode = false,
|
||||
@ -192,7 +193,7 @@ class NodeConfigurationImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `rpcAddress and rpcSettings_address are equivalent`() {
|
||||
var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
rawConfig = rawConfig.withoutPath("rpcSettings.address")
|
||||
@ -201,8 +202,8 @@ class NodeConfigurationImplTest {
|
||||
assertThat(rawConfig.parseAsNodeConfiguration().isValid).isTrue()
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
fun `relative path correctly parsed`() {
|
||||
@Test(timeout=3_000)
|
||||
fun `absolute base directory leads to correct cordapp directories`() {
|
||||
val rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
|
||||
// Override base directory to have predictable experience on diff OSes
|
||||
@ -213,13 +214,69 @@ class NodeConfigurationImplTest {
|
||||
.resolve()
|
||||
|
||||
val nodeConfiguration = finalConfig.parseAsNodeConfiguration()
|
||||
assertThat(nodeConfiguration.isValid).isTrue()
|
||||
assertTrue(nodeConfiguration.isValid)
|
||||
|
||||
val baseDirPath = tempFolder.root.toPath()
|
||||
assertEquals(listOf(baseDirPath / "./myCorDapps1", baseDirPath / "./myCorDapps2"), nodeConfiguration.value().cordappDirectories)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `absolute base directory leads to correct default cordapp directory`() {
|
||||
val rawConfig = ConfigFactory.parseResources("working-config-no-cordapps.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
|
||||
// Override base directory to have predictable experience on diff OSes
|
||||
val finalConfig = configOf(
|
||||
// Add substitution values here
|
||||
"baseDirectory" to tempFolder.root.canonicalPath)
|
||||
.withFallback(rawConfig)
|
||||
.resolve()
|
||||
|
||||
val nodeConfiguration = finalConfig.parseAsNodeConfiguration()
|
||||
assertTrue(nodeConfiguration.isValid)
|
||||
|
||||
val baseDirPath = tempFolder.root.toPath()
|
||||
assertEquals(listOf(baseDirPath / "cordapps"), nodeConfiguration.value().cordappDirectories)
|
||||
}
|
||||
|
||||
@Test(timeout=3_000)
|
||||
fun `relative base dir leads to correct cordapp directories`() {
|
||||
val rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
|
||||
val path = tempFolder.root.relativeTo(tempFolder.root.parentFile).toString()
|
||||
val fullPath = File(".").resolve(path).toString()
|
||||
// Override base directory to have predictable experience on diff OSes
|
||||
val finalConfig = configOf(
|
||||
// Add substitution values here
|
||||
"baseDirectory" to fullPath)
|
||||
.withFallback(rawConfig)
|
||||
.resolve()
|
||||
|
||||
val nodeConfiguration = finalConfig.parseAsNodeConfiguration()
|
||||
assertTrue(nodeConfiguration.isValid)
|
||||
|
||||
assertEquals(listOf(fullPath / "./myCorDapps1", fullPath / "./myCorDapps2"), nodeConfiguration.value().cordappDirectories)
|
||||
}
|
||||
|
||||
@Test(timeout=3_000)
|
||||
fun `relative base dir leads to correct default cordapp directory`() {
|
||||
val rawConfig = ConfigFactory.parseResources("working-config-no-cordapps.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
|
||||
val path = tempFolder.root.relativeTo(tempFolder.root.parentFile).toString()
|
||||
val fullPath = File(".").resolve(path).toString()
|
||||
// Override base directory to have predictable experience on diff OSes
|
||||
val finalConfig = configOf(
|
||||
// Add substitution values here
|
||||
"baseDirectory" to fullPath)
|
||||
.withFallback(rawConfig)
|
||||
.resolve()
|
||||
|
||||
val nodeConfiguration = finalConfig.parseAsNodeConfiguration()
|
||||
assertTrue(nodeConfiguration.isValid)
|
||||
|
||||
assertEquals(listOf(fullPath / "cordapps"), nodeConfiguration.value().cordappDirectories)
|
||||
}
|
||||
|
||||
@Test(timeout=3_000)
|
||||
fun `missing rpcSettings_adminAddress cause a graceful failure`() {
|
||||
var rawConfig = ConfigFactory.parseResources("working-config.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
rawConfig = rawConfig.withoutPath("rpcSettings.adminAddress")
|
||||
@ -229,7 +286,7 @@ class NodeConfigurationImplTest {
|
||||
assertThat(config.errors.asSequence().map(Configuration.Validation.Error::message).filter { it.contains("rpcSettings.adminAddress") }.toList()).isNotEmpty
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `compatibilityZoneURL populates NetworkServices`() {
|
||||
val compatibilityZoneURL = URI.create("https://r3.com").toURL()
|
||||
val configuration = testConfiguration.copy(
|
||||
@ -241,14 +298,14 @@ class NodeConfigurationImplTest {
|
||||
assertEquals(compatibilityZoneURL, configuration.networkServices!!.networkMapURL)
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `jmxReporterType is null and defaults to Jokolia`() {
|
||||
val rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||
val nodeConfig = rawConfig.parseAsNodeConfiguration().value()
|
||||
assertEquals(JmxReporterType.JOLOKIA.toString(), nodeConfig.jmxReporterType.toString())
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `jmxReporterType is not null and is set to New Relic`() {
|
||||
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||
rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("NEW_RELIC"))
|
||||
@ -256,7 +313,7 @@ class NodeConfigurationImplTest {
|
||||
assertEquals(JmxReporterType.NEW_RELIC.toString(), nodeConfig.jmxReporterType.toString())
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `jmxReporterType is not null and set to Jokolia`() {
|
||||
var rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("devMode" to true)))
|
||||
rawConfig = rawConfig.withValue("jmxReporterType", ConfigValueFactory.fromAnyRef("JOLOKIA"))
|
||||
@ -264,7 +321,7 @@ class NodeConfigurationImplTest {
|
||||
assertEquals(JmxReporterType.JOLOKIA.toString(), nodeConfig.jmxReporterType.toString())
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `network services`() {
|
||||
val rawConfig = getConfig("test-config-with-networkservices.conf")
|
||||
val nodeConfig = rawConfig.parseAsNodeConfiguration().value()
|
||||
@ -276,7 +333,7 @@ class NodeConfigurationImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout=300_000)
|
||||
@Test(timeout=3_000)
|
||||
fun `check crlCheckArtemisServer flag`() {
|
||||
assertFalse(getConfig("working-config.conf").parseAsNodeConfiguration().value().crlCheckArtemisServer)
|
||||
val rawConfig = getConfig("working-config.conf", ConfigFactory.parseMap(mapOf("crlCheckArtemisServer" to true)))
|
||||
|
31
node/src/test/resources/working-config-no-cordapps.conf
Normal file
31
node/src/test/resources/working-config-no-cordapps.conf
Normal file
@ -0,0 +1,31 @@
|
||||
myLegalName = "O=Alice Corp, L=Madrid, C=ES"
|
||||
emailAddress = "admin@company.com"
|
||||
keyStorePassword = "cordacadevpass"
|
||||
trustStorePassword = "trustpass"
|
||||
crlCheckSoftFail = true
|
||||
baseDirectory = "/opt/corda"
|
||||
dataSourceProperties = {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
dataSource.url = "jdbc:h2:file:blah"
|
||||
dataSource.user = "sa"
|
||||
dataSource.password = ""
|
||||
}
|
||||
database = {
|
||||
transactionIsolationLevel = "REPEATABLE_READ"
|
||||
exportHibernateJMXStatistics = "false"
|
||||
}
|
||||
p2pAddress = "localhost:2233"
|
||||
h2port = 0
|
||||
useTestClock = false
|
||||
verifierType = InMemory
|
||||
rpcSettings = {
|
||||
address = "locahost:3418"
|
||||
adminAddress = "localhost:3419"
|
||||
useSsl = false
|
||||
standAloneBroker = false
|
||||
}
|
||||
flowTimeout {
|
||||
timeout = 30 seconds
|
||||
maxRestartCount = 3
|
||||
backoffBase = 2.0
|
||||
}
|
@ -27,6 +27,7 @@ object UpdateBusinessDayFlow {
|
||||
override fun call() {
|
||||
val message = otherPartySession.receive<UpdateBusinessDayMessage>().unwrap { it }
|
||||
(serviceHub.clock as DemoClock).updateDate(message.date)
|
||||
otherPartySession.send(true) // Let's Broadcast know we've updated the clock
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +65,7 @@ object UpdateBusinessDayFlow {
|
||||
|
||||
@Suspendable
|
||||
private fun doNextRecipient(recipient: Party) {
|
||||
initiateFlow(recipient).send(UpdateBusinessDayMessage(date))
|
||||
initiateFlow(recipient).sendAndReceive<Boolean>(UpdateBusinessDayMessage(date))
|
||||
}
|
||||
}
|
||||
}
|
@ -61,8 +61,6 @@ include 'tools:network-builder'
|
||||
include 'tools:cliutils'
|
||||
include 'tools:worldmap'
|
||||
include 'tools:checkpoint-agent'
|
||||
include 'example-code'
|
||||
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
|
||||
include 'samples:attachment-demo:contracts'
|
||||
include 'samples:attachment-demo:workflows'
|
||||
include 'samples:trader-demo:workflows-trader'
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM corda/corda-zulu-4.1
|
||||
FROM corda/corda-zulu-java1.8-4.4
|
||||
|
||||
# Copy corda files
|
||||
ADD --chown=corda:corda node.conf /opt/corda/node.conf
|
||||
@ -18,4 +18,4 @@ WORKDIR /opt/corda
|
||||
ENV HOME=/opt/corda
|
||||
|
||||
# Start it
|
||||
CMD ["run-corda"]
|
||||
CMD ["run-corda"]
|
||||
|
Loading…
Reference in New Issue
Block a user