mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
parent
c897280143
commit
550b446b87
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -61,8 +61,6 @@ include 'tools:network-builder'
|
|||||||
include 'tools:cliutils'
|
include 'tools:cliutils'
|
||||||
include 'tools:worldmap'
|
include 'tools:worldmap'
|
||||||
include 'tools:checkpoint-agent'
|
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:contracts'
|
||||||
include 'samples:attachment-demo:workflows'
|
include 'samples:attachment-demo:workflows'
|
||||||
include 'samples:trader-demo:workflows-trader'
|
include 'samples:trader-demo:workflows-trader'
|
||||||
|
Loading…
Reference in New Issue
Block a user