diff --git a/java-samples/corda5-negotiation-cordapp/README.md b/java-samples/corda5-negotiation-cordapp/README.md index 4541221..54860d4 100644 --- a/java-samples/corda5-negotiation-cordapp/README.md +++ b/java-samples/corda5-negotiation-cordapp/README.md @@ -4,7 +4,6 @@ This CorDapp shows how multi-party negotiation is handled on the Corda ledger, i interaction. ## Concepts - A flow is provided that allows a node to propose a trade to a counterparty. The counterparty has two options: * Accepting the proposal, converting the `ProposalState` into a `TradeState` with identical attributes @@ -12,39 +11,36 @@ A flow is provided that allows a node to propose a trade to a counterparty. The amount Only the recipient of the proposal has the ability to accept it or modify it. If the sender of the proposal tries to -accept or modify the proposal, this attempt will be rejected automatically at the flow level. +accept or modify the proposal, this attempt will be rejected automatically at the flow level. Similarly, the modifier +cannot accept the modified proposal. ### Flows +We start with the proposal flow implemented in `ProposalFlowRequest.java`. -We start with the proposal flow implemented in `ProposalFlow.java`. - - -The modification of the proposal is implemented in `ModificationFlow.java`. - - -In the `AcceptanceFlow.java`, we receive the modified ProposalState and it's converted into a TradeState. - +The modification of the proposal is implemented in `ModifyFlowRequest.java`. +In the `AcceptFlowRequest.java`, we receive the modified or unmodified ProposalState and it is converted into a +TradeState. ### Setting up -1. We will begin our test deployment with clicking the `startCorda`. This task will load up the combined Corda workers in docker. - A successful deployment will allow you to open the REST APIs at: https://localhost:8888/api/v1/swagger#. You can test out some - functions to check connectivity. (GET /cpi function call should return an empty list as for now.) -2. We will now deploy the cordapp with a click of `5-vNodeSetup` task. Upon successful deployment of the CPI, the GET /cpi function call should now return the meta data of the cpi you just upload. - - - -### Running the app +1. We begin our test deployment with clicking the `startCorda`. This task loads up the combined Corda workers in Docker. + A successful deployment will allow you to open the REST APIs at: https://localhost:8888/api/v1/swagger#. You can test + out some functions to check connectivity. (GET /cpi function call should return an empty list as for now.) +2. We now deploy the CorDapp with a click of `5-vNodeSetup` task. Upon successful deployment of the CPI, the GET /cpi + function call returns the metadata of the CPI you just upload. -In Corda 5, flows will be triggered via `POST /flow/{holdingidentityshorthash}` and flow result will need to be view at `GET /flow/{holdingidentityshorthash}/{clientrequestid}` -* holdingidentityshorthash: the id of the network participants, ie Bob, Alice, Charlie. You can view all the short hashes of the network member with another gradle task called `ListVNodes` -* clientrequestid: the id you specify in the flow requestBody when you trigger a flow. +### Running the CorDapp +In Corda 5, flows are triggered via `POST /flow/{holdingidentityshorthash}` and flow result has to be viewed at +`GET /flow/{holdingidentityshorthash}/{clientrequestid}` +* holdingidentityshorthash: the ID of the network participants, i.e. Bob, Alice, Charlie. You can view all the short + hashes of the network member with another Gradle task called `ListVNodes` +* clientrequestid: the ID you specify in the flow request body when you trigger a flow. -#### Step 1: Create ProposalState between two parties -Pick a VNode identity to initiate the Proposal creation, and get its short hash. (Let's pick Alice.). +#### Step 1: Create Proposal state between two parties +Pick a VNode identity to initiate the Proposal state creation and get its short hash. For example, let's pick Alice: -Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +Go to `POST /flow/{holdingidentityshorthash}`, enter Alice's identity short hash and the following request body: ``` { "clientRequestId": "createProposal", @@ -55,12 +51,13 @@ Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Ali } } ``` -After trigger the create-ProposalFlow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and client request id ("createProposal" in the case above) to view the flow result. - +After triggering the create-ProposalFlow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter +Alice's identity short hash and client request ID ("createProposal" in the case above) to view the flow result. #### Step 2: List created Proposal state -In order to continue the app logics, we would need the Proposal ID. This step will bring out all the Proposal this entity (Alice) has. -Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +In order to continue the CorDapp's logics, we need the Proposal ID - the identity of the created Proposal state. This +step will bring out all the Proposal this entity (Alice) has. +Go to `POST /flow/{holdingidentityshorthash}`, enter the Alice's identity short hash and request body: ``` { "clientRequestId": "list-1", @@ -68,12 +65,12 @@ Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Ali "requestBody": {} } ``` -After trigger the List Proposal, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and client request id ("list-1" in the case above) to view the flow result. - +After triggering the List Proposal, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter Alice's +identity short hash and client request id ("list-1" in the case above) to view the flow result. #### Step 3: Modify the proposal -In order to continue the app logics, we would need the Proposal ID. This step will bring out the Proposal entries this entity (Alice) has. Bob can edit the proposal if required by entering the new amount. -Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Bob hash) and request body: +The responder, Bob, can edit the proposal if required by entering the new amount. +Go to `POST /flow/{holdingidentityshorthash}`, enter Bob's identity short hash and request body: ``` { "clientRequestId": "ModifyFlow", @@ -84,22 +81,26 @@ Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Bob } } ``` -After triggering the modify flow we need to hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and check the result. Enter bob's hash id and the modify flow id which is "ModifyFlow" in the case above. - +After triggering the modify flow, we need to hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and +check the result. Enter Bob's hash id and the modify flow ID which is "ModifyFlow" in the case above. #### Step 4: Accept the new proposal from bob `AcceptFlow` -In this step, alice will accept the new proposal of Bob. -Goto `POST /flow/{holdingidentityshorthash}`, enter the identity short hash (of Alice) and request body, we also need to provide the proposalId, which is same as the proposal ID used in modifyFlow body. +In this step, Alice will accept the new proposal from Bob. +Goto `POST /flow/{holdingidentityshorthash}`, enter Alice's identity short hash and request body, we also need to +provide the proposalId, which is same as the proposal ID used in ModifyFlow body and also Alice's own details +(this is because the Accept smart contract requires this information) ``` { "clientRequestId": "AcceptFlow", "flowClassName": "com.r3.developers.samples.negotiation.workflows.accept.AcceptFlowRequest", "requestBody": { - "proposalID": "" + "proposalID": "", + "acceptor": "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB" } } ``` -And as for the result of this flow, go to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the required fields. +And as for the result of this flow, go to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the +required fields. Thus, we have concluded a full run through of the Negotiation app. @@ -108,13 +109,8 @@ Below are the app diagrams which are useful for the visual understanding. #### Dynamic Diagram - ![img_2.png](negotiation-sequence-diagram.png) - - - - #### Static Diagram ![img.png](negotiation-design-diagram.png) diff --git a/java-samples/corda5-negotiation-cordapp/build.gradle b/java-samples/corda5-negotiation-cordapp/build.gradle index 3fa6225..75c8e74 100644 --- a/java-samples/corda5-negotiation-cordapp/build.gradle +++ b/java-samples/corda5-negotiation-cordapp/build.gradle @@ -13,6 +13,13 @@ allprojects { group 'net.corda.samples' version '1.0-SNAPSHOT' + configurations.configureEach { + resolutionStrategy { + // FORCE Gradle to use latest SNAPSHOT versions. + cacheChangingModulesFor 0, 'seconds' + } + } + def javaVersion = VERSION_11 // Configure the CSDE @@ -46,12 +53,36 @@ allprojects { // All dependencies are held in Maven Central mavenLocal() mavenCentral() + // Remove once Corda 5.1 and Flow Testing Driver are full GA + maven { + url = "$artifactoryContextUrl/corda-dependencies-dev" + authentication { + basic(BasicAuthentication) + } + credentials { + username = findProperty('cordaArtifactoryUsername') ?: System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = findProperty('cordaArtifactoryPassword') ?: System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + mavenContent { + snapshotsOnly() + } + } + // Remove once Corda 5.1 and Flow Testing Driver are full GA + maven { + url = "$artifactoryContextUrl/corda-os-maven" + authentication { + basic(BasicAuthentication) + } + credentials { + username = findProperty('cordaArtifactoryUsername') ?: System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = findProperty('cordaArtifactoryPassword') ?: System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + } } tasks.withType(Test).configureEach { useJUnitPlatform() } - } publishing { @@ -61,7 +92,5 @@ publishing { groupId project.group artifact jar } - } } - diff --git a/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Proposal.java b/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Proposal.java index 281ed43..058b39c 100644 --- a/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Proposal.java +++ b/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Proposal.java @@ -19,23 +19,26 @@ public class Proposal implements ContractState { private final Member proposer; private final Member proposee; private final UUID proposalID; + private final Member modifier; @ConstructorForDeserialization - public Proposal(int amount, Member buyer, Member seller, Member proposer, Member proposee, UUID proposalID) { + public Proposal(int amount, Member buyer, Member seller, Member proposer, Member proposee, Member modifier, UUID proposalID) { this.amount = amount; this.buyer = buyer; this.seller = seller; this.proposee = proposee; this.proposer = proposer; + this.modifier = modifier; this.proposalID = proposalID; } - public Proposal(int amount, Member buyer, Member seller, Member proposer, Member proposee) { + public Proposal(int amount, Member buyer, Member seller, Member proposer, Member proposee, Member modifier) { this.amount = amount; this.buyer = buyer; this.seller = seller; this.proposee = proposee; this.proposer = proposer; + this.modifier = modifier; this.proposalID = UUID.randomUUID(); } @@ -63,6 +66,10 @@ public UUID getProposalID() { return proposalID; } + public Member getModifier() { + return modifier; + } + @NotNull @Override public List getParticipants() { diff --git a/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/ProposalAndTradeContract.java b/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/ProposalAndTradeContract.java index 9ce0cd7..fbb4b3b 100644 --- a/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/ProposalAndTradeContract.java +++ b/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/ProposalAndTradeContract.java @@ -101,6 +101,7 @@ public static class Accept implements NegotiationCommands { String sellerMsg = "The seller is unmodified in the output"; String proposerMsg = "The proposer is a required signer"; String proposeMsg = "The propose is a required signer"; + String proposerCannotAcceptProposalMsg = "The proposer cannot accept their own proposal"; @Override public void verify(UtxoLedgerTransaction transaction) { @@ -112,6 +113,9 @@ public void verify(UtxoLedgerTransaction transaction) { require(transaction.getOutputTransactionStates().size() == 1, oneOutputMsg); require(transaction.getOutputStates(Trade.class).size() == 1, outputTypeMsg); require(transaction.getCommands().size() == 1, oneCommandMsg); + require(proposalStateInputs.getModifier() == null || + !proposalStateInputs.getModifier().getName().toString().equals(tradeStateOutput.getAcceptor().toString()), + proposerCannotAcceptProposalMsg); require(tradeStateOutput.getAmount() == proposalStateInputs.getAmount(), amountMsg); require(proposalStateInputs.getBuyer().toString().equals(tradeStateOutput.getBuyer().toString()), buyerMsg); @@ -119,7 +123,6 @@ public void verify(UtxoLedgerTransaction transaction) { require(transaction.getSignatories().contains(proposalStateInputs.getProposer().getLedgerKey()), proposerMsg); require(transaction.getSignatories().contains(proposalStateInputs.getProposee().getLedgerKey()), proposeMsg); - } } diff --git a/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Trade.java b/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Trade.java index aebe2cb..50dea88 100644 --- a/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Trade.java +++ b/java-samples/corda5-negotiation-cordapp/contracts/src/main/java/com/r3/developers/samples/negotiation/Trade.java @@ -2,6 +2,7 @@ import com.r3.developers.samples.negotiation.util.Member; import net.corda.v5.base.annotations.ConstructorForDeserialization; +import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.ledger.utxo.BelongsToContract; import net.corda.v5.ledger.utxo.ContractState; import org.jetbrains.annotations.NotNull; @@ -17,27 +18,19 @@ public class Trade implements ContractState { private final int amount; private final Member buyer; private final Member seller; - private final UUID proposalID; + private final UUID tradeID; + private final MemberX500Name acceptor; public List participants; - @ConstructorForDeserialization - public Trade(int amount, Member buyer, Member seller, UUID proposalID, List participants) { + public Trade(int amount, Member buyer, Member seller, MemberX500Name acceptor, List participants) { this.amount = amount; this.buyer = buyer; this.seller = seller; - this.proposalID = proposalID; + this.acceptor = acceptor; + this.tradeID = UUID.randomUUID(); this.participants = participants; } - public Trade(int amount, Member buyer, Member seller, List participants) { - this.amount = amount; - this.buyer = buyer; - this.seller = seller; - this.proposalID = UUID.randomUUID(); - this.participants = participants; - } - - public int getAmount() { return amount; } @@ -50,8 +43,11 @@ public Member getBuyer() { return buyer; } - public UUID getProposalID() { - return proposalID; + public UUID getTradeID() { + return tradeID; + } + public MemberX500Name getAcceptor() { + return acceptor; } @NotNull diff --git a/java-samples/corda5-negotiation-cordapp/gradle.properties b/java-samples/corda5-negotiation-cordapp/gradle.properties index 5aa4933..a0da79b 100644 --- a/java-samples/corda5-negotiation-cordapp/gradle.properties +++ b/java-samples/corda5-negotiation-cordapp/gradle.properties @@ -2,14 +2,16 @@ kotlin.code.style=official # Specify the version of the Corda-API to use. # This needs to match the version supported by the Corda Cluster the CorDapp will run on. -cordaApiVersion=5.0.0.765 +cordaApiVersion=5.1.0.16-beta-+ # Specify the version of the notary plugins to use. # Currently packaged as part of corda-runtime-os, so should be set to a corda-runtime-os version. -cordaNotaryPluginsVersion=5.0.0.0 +cordaNotaryPluginsVersion=5.1.0.0-+ # Specify the version of the Combined Worker to use -combinedWorkerJarVersion=5.0.0.0 +combinedWorkerJarVersion=5.1.0.0-HC05 +# Specify the version of the Flow Testing Driver to use (only works with Corda 5.1 artifacts) +cordaDriverVersion=5.1.0-DRIVER.0-+ # Specify the version of the cordapp-cpb and cordapp-cpk plugins cordaPluginsVersion=7.0.3 @@ -35,7 +37,8 @@ junitVersion = 5.8.2 mockitoKotlinVersion=4.0.0 mockitoVersion=4.6.1 hamcrestVersion=2.2 - +assertjVersion = 3.24.1 +jacksonVersion=2.15.2 # R3 internal repository # Use this version when pointing to artefacts in artifactory that have not been published to S3 diff --git a/java-samples/corda5-negotiation-cordapp/settings.gradle b/java-samples/corda5-negotiation-cordapp/settings.gradle index d039a95..d8394e0 100644 --- a/java-samples/corda5-negotiation-cordapp/settings.gradle +++ b/java-samples/corda5-negotiation-cordapp/settings.gradle @@ -1,6 +1,21 @@ pluginManagement { // Declare the repositories where plugins are stored. repositories { + // Remove once Corda 5.1 and Flow Testing Driver are full GA + maven { + url = "$artifactoryContextUrl/corda-os-maven" + content { + includeGroupByRegex 'net\\.corda\\.cordapp(\\..*)?' + } + authentication { + basic(BasicAuthentication) + } + credentials { + username = settings.ext.find('cordaArtifactoryUsername') ?: System.getenv('CORDA_ARTIFACTORY_USERNAME') + password = settings.ext.find('cordaArtifactoryPassword') ?: System.getenv('CORDA_ARTIFACTORY_PASSWORD') + } + } + // gradlePluginPortal() mavenCentral() mavenLocal() diff --git a/java-samples/corda5-negotiation-cordapp/workflows/build.gradle b/java-samples/corda5-negotiation-cordapp/workflows/build.gradle index 9cb4157..9fa26b4 100644 --- a/java-samples/corda5-negotiation-cordapp/workflows/build.gradle +++ b/java-samples/corda5-negotiation-cordapp/workflows/build.gradle @@ -38,15 +38,43 @@ dependencies { // 3rd party libraries // Required - testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.slf4j:slf4j-simple:1.7.36" testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - // Optional but used by exmaple tests. + // Optional but used by example tests. testImplementation "org.mockito:mockito-core:$mockitoVersion" testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" + testImplementation "org.assertj:assertj-core:$assertjVersion" + + testImplementation "net.corda:corda-driver:$cordaDriverVersion" + testRuntimeOnly "net.corda:corda-driver-engine:$cordaDriverVersion" + testRuntimeOnly files(configurations.archives.artifacts.files) + testRuntimeOnly "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-server:$cordaNotaryPluginsVersion:package@cpb" + +} + +tasks.withType(Test).configureEach { + doFirst { + jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/java.lang.invoke=ALL-UNNAMED', + '--add-opens', 'java.base/java.nio=ALL-UNNAMED', + '--add-opens', 'java.base/java.time=ALL-UNNAMED', + '--add-opens', 'java.base/java.util=ALL-UNNAMED' + + systemProperty 'co.paralleluniverse.fibers.verifyInstrumentation', true + systemProperty 'java.io.tmpdir', buildDir.absolutePath + + systemProperty 'org.slf4j.simpleLogger.defaultLogLevel', 'info' + systemProperty 'org.slf4j.simpleLogger.dateTimeFormat', 'yyyy-MM-dd HH:mm:ss:SSS Z' + systemProperty 'org.slf4j.simpleLogger.showDateTime', true + systemProperty 'org.slf4j.simpleLogger.showShortLogName', true + systemProperty 'org.slf4j.simpleLogger.showThreadName', false + systemProperty 'org.slf4j.simpleLogger.logFile', 'System.out' + systemProperty 'org.slf4j.simpleLogger.log.org.apache.aries.spifly.BaseActivator', 'OFF' + } } // The CordApp section. diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowArgs.java b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowArgs.java index bd58d40..c55d452 100644 --- a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowArgs.java +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowArgs.java @@ -1,6 +1,7 @@ package com.r3.developers.samples.negotiation.workflows.accept; import net.corda.v5.base.annotations.CordaSerializable; +import net.corda.v5.base.types.MemberX500Name; import java.util.UUID; @@ -8,17 +9,21 @@ public class AcceptFlowArgs { private UUID proposalID; + private MemberX500Name acceptor; public AcceptFlowArgs() { } - public AcceptFlowArgs(UUID proposalId) { + public AcceptFlowArgs(UUID proposalId, MemberX500Name acceptor) { this.proposalID = proposalId; + this.acceptor = acceptor; } public UUID getProposalID() { return proposalID; } - + public MemberX500Name getAcceptor() { + return acceptor; + } } diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowRequest.java b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowRequest.java index 9fbaff2..530a1d5 100644 --- a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowRequest.java +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/accept/AcceptFlowRequest.java @@ -4,7 +4,7 @@ import com.r3.developers.samples.negotiation.ProposalAndTradeContract; import com.r3.developers.samples.negotiation.Trade; import com.r3.developers.samples.negotiation.util.Member; -import com.r3.developers.samples.negotiation.workflows.util.FInalizeFlow; +import com.r3.developers.samples.negotiation.workflows.util.FinalizeFlow; import net.corda.v5.application.flows.*; import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.membership.MemberLookup; @@ -66,7 +66,7 @@ public String call(@NotNull ClientRequestBody requestBody) { Trade output = new Trade(proposalInput.getAmount(), new Member(proposalInput.getBuyer().getName(), proposalInput.getBuyer().getLedgerKey()), new Member(proposalInput.getSeller().getName(), proposalInput.getSeller().getLedgerKey()), - proposalInput.getParticipants() + request.getAcceptor(), proposalInput.getParticipants() ); Member counterParty = (memberLookup.myInfo().getName().equals(proposalInput.getProposer().getName())) ? proposalInput.getProposee() : proposalInput.getProposer(); @@ -87,8 +87,8 @@ public String call(@NotNull ClientRequestBody requestBody) { try { UtxoSignedTransaction signedTransaction = transactionBuilder.toSignedTransaction(); FlowSession counterPartySession = flowMessaging.initiateFlow(counterParty.getName()); - return flowEngine.subFlow(new FInalizeFlow.FinalizeRequest(signedTransaction, List.of(counterPartySession))); - + flowEngine.subFlow(new FinalizeFlow.FinalizeRequest(signedTransaction, List.of(counterPartySession))); + return output.getTradeID().toString(); } catch (Exception e) { throw new CordaRuntimeException(e.getMessage()); } diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/modify/ModifyFlowRequest.java b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/modify/ModifyFlowRequest.java index eb04262..8aa49dc 100644 --- a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/modify/ModifyFlowRequest.java +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/modify/ModifyFlowRequest.java @@ -1,10 +1,9 @@ package com.r3.developers.samples.negotiation.workflows.modify; - import com.r3.developers.samples.negotiation.Proposal; import com.r3.developers.samples.negotiation.ProposalAndTradeContract; import com.r3.developers.samples.negotiation.util.Member; -import com.r3.developers.samples.negotiation.workflows.util.FInalizeFlow; +import com.r3.developers.samples.negotiation.workflows.util.FinalizeFlow; import net.corda.v5.application.flows.*; import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.membership.MemberLookup; @@ -70,11 +69,13 @@ public String call(@NotNull ClientRequestBody requestBody) { //creating a new Proposal as an output state Member counterParty = (memberLookup.myInfo().getName().equals(proposalInput.getProposer().getName())) ? proposalInput.getProposee() : proposalInput.getProposer(); + Proposal output = new Proposal(request.getNewAmount(), new Member(proposalInput.getBuyer().getName(), proposalInput.getBuyer().getLedgerKey()), new Member(proposalInput.getSeller().getName(), proposalInput.getSeller().getLedgerKey()), new Member(memberLookup.myInfo().getName(), memberLookup.myInfo().getLedgerKeys().get(0)), new Member(counterParty.getName(), counterParty.getLedgerKey()), + new Member(memberLookup.myInfo().getName(), memberLookup.myInfo().getLedgerKeys().get(0)), proposalID); // Initiating the transactionBuilder with command to "modify" @@ -93,8 +94,8 @@ public String call(@NotNull ClientRequestBody requestBody) { try { UtxoSignedTransaction signedTransaction = transactionBuilder.toSignedTransaction(); FlowSession counterPartySession = flowMessaging.initiateFlow(counterParty.getName()); - return flowEngine.subFlow(new FInalizeFlow.FinalizeRequest(signedTransaction, List.of(counterPartySession))); - + flowEngine.subFlow(new FinalizeFlow.FinalizeRequest(signedTransaction, List.of(counterPartySession))); + return output.getProposalID().toString(); } catch (Exception e) { throw new CordaRuntimeException(e.getMessage()); } diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowArgs.java b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowArgs.java index 973d107..e636ec2 100644 --- a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowArgs.java +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowArgs.java @@ -1,19 +1,20 @@ package com.r3.developers.samples.negotiation.workflows.propose; import net.corda.v5.base.annotations.CordaSerializable; +import net.corda.v5.base.types.MemberX500Name; @CordaSerializable public class ProposalFlowArgs { private int amount; - private String counterParty; + private MemberX500Name counterParty; private boolean isBuyer; public ProposalFlowArgs() { } - public ProposalFlowArgs(int amount, String counterParty, boolean isBuyer) { + public ProposalFlowArgs(int amount, MemberX500Name counterParty, boolean isBuyer) { this.amount = amount; this.counterParty = counterParty; this.isBuyer = isBuyer; @@ -23,13 +24,11 @@ public int getAmount() { return amount; } - public String getCounterParty() { + public MemberX500Name getCounterParty() { return counterParty; } - public boolean isBuyer() { + public boolean getIsBuyer() { return isBuyer; } - - } diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowRequest.java b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowRequest.java index 6aa773a..1c33b60 100644 --- a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowRequest.java +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/propose/ProposalFlowRequest.java @@ -3,7 +3,7 @@ import com.r3.developers.samples.negotiation.Proposal; import com.r3.developers.samples.negotiation.ProposalAndTradeContract; import com.r3.developers.samples.negotiation.util.Member; -import com.r3.developers.samples.negotiation.workflows.util.FInalizeFlow; +import com.r3.developers.samples.negotiation.workflows.util.FinalizeFlow; import net.corda.v5.application.flows.*; import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.membership.MemberLookup; @@ -24,6 +24,7 @@ import java.time.Instant; import java.util.List; import java.util.Objects; +import java.util.UUID; @InitiatingFlow(protocol = "proposal") public class ProposalFlowRequest implements ClientStartableFlow { @@ -50,16 +51,15 @@ public class ProposalFlowRequest implements ClientStartableFlow { @Suspendable @Override public String call(@NotNull ClientRequestBody requestBody) { - // Obtain the deserialized input arguments to the flow from the requestBody. ProposalFlowArgs request = requestBody.getRequestBodyAs(jsonMarshallingService, ProposalFlowArgs.class); MemberX500Name buyer; MemberX500Name seller; - MemberInfo memberInfo = memberLookup.lookup(MemberX500Name.parse(request.getCounterParty())); + MemberInfo memberInfo = memberLookup.lookup(request.getCounterParty()); Member counterParty = new Member(Objects.requireNonNull(memberInfo).getName(), memberInfo.getLedgerKeys().get(0)); - if (request.isBuyer()) { + if (request.getIsBuyer()) { buyer = memberLookup.myInfo().getName(); seller = memberInfo.getName(); } else { @@ -72,8 +72,8 @@ public String call(@NotNull ClientRequestBody requestBody) { new Member(seller, Objects.requireNonNull(memberLookup.lookup(seller)).getLedgerKeys().get(0)), new Member(buyer, Objects.requireNonNull(memberLookup.lookup(seller)).getLedgerKeys().get(0)), new Member(memberLookup.myInfo().getName(), memberLookup.myInfo().getLedgerKeys().get(0)), - counterParty); - + counterParty, null); + UUID proposalStateId = output.getProposalID(); NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); @@ -92,8 +92,8 @@ public String call(@NotNull ClientRequestBody requestBody) { try { UtxoSignedTransaction signedTransaction = transactionBuilder.toSignedTransaction(); FlowSession counterPartySession = flowMessaging.initiateFlow(counterParty.getName()); - return flowEngine.subFlow(new FInalizeFlow.FinalizeRequest(signedTransaction, List.of(counterPartySession))); - + flowEngine.subFlow(new FinalizeFlow.FinalizeRequest(signedTransaction, List.of(counterPartySession))); + return proposalStateId.toString(); } catch (Exception e) { throw new CordaRuntimeException(e.getMessage()); } diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/util/FInalizeFlow.java b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/util/FinalizeFlow.java similarity index 79% rename from java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/util/FInalizeFlow.java rename to java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/util/FinalizeFlow.java index db47980..cdb7cc1 100644 --- a/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/util/FInalizeFlow.java +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/main/java/com/r3/developers/samples/negotiation/workflows/util/FinalizeFlow.java @@ -14,8 +14,8 @@ import java.util.List; -public class FInalizeFlow { - private final static Logger log = LoggerFactory.getLogger(FInalizeFlow.class); +public class FinalizeFlow { + private final static Logger log = LoggerFactory.getLogger(FinalizeFlow.class); @InitiatingFlow(protocol = "finalize-protocol") public static class FinalizeRequest implements SubFlow { @@ -62,7 +62,6 @@ public String call() { @InitiatedBy(protocol = "finalize-protocol") public static class FinalizeResponder implements ResponderFlow { - // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. @CordaInject public UtxoLedgerService utxoLedgerService; @@ -70,23 +69,13 @@ public static class FinalizeResponder implements ResponderFlow { @Override @Suspendable public void call(@NotNull FlowSession session) { - String proposalError = "Only the proposee can modify or accept a proposal."; - String successMessage = "Successfully finished modification responder flow - "; + String successMessage = "Successfully finished responder flow - "; try { UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService.receiveFinality(session, transaction -> { - // goes into this if block is command is either modify or accept - if (!transaction.getInputStates(Proposal.class).isEmpty()) { - MemberX500Name proposee = transaction.getInputStates(Proposal.class).get(0).getProposee().getName(); - if (!proposee.toString().equals(session.getCounterparty().toString())) { - throw new CordaRuntimeException(proposalError); - } - } }).getTransaction(); log.info(successMessage + finalizedSignedTransaction.getId()); - - } // Soft fails the flow and log the exception. catch (Exception e) { diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/MemberX500NameDeserializer.java b/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/MemberX500NameDeserializer.java new file mode 100644 index 0000000..6a3b831 --- /dev/null +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/MemberX500NameDeserializer.java @@ -0,0 +1,15 @@ +package com.r3.developers.samples.negotiation.workflows; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import net.corda.v5.base.types.MemberX500Name; + +import java.io.IOException; + +public class MemberX500NameDeserializer extends JsonDeserializer { + @Override + public MemberX500Name deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException { + return MemberX500Name.parse(parser.getText()); + } +} diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/MemberX500NameSerializer.java b/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/MemberX500NameSerializer.java new file mode 100644 index 0000000..7b64fb7 --- /dev/null +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/MemberX500NameSerializer.java @@ -0,0 +1,15 @@ +package com.r3.developers.samples.negotiation.workflows; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import net.corda.v5.base.types.MemberX500Name; + +import java.io.IOException; + +public class MemberX500NameSerializer extends JsonSerializer { + @Override + public void serialize(MemberX500Name value, JsonGenerator generator, SerializerProvider serializers) throws IOException { + generator.writeString(value.toString()); + } +} diff --git a/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/NegotiationFlowTests.java b/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/NegotiationFlowTests.java new file mode 100644 index 0000000..eef3e36 --- /dev/null +++ b/java-samples/corda5-negotiation-cordapp/workflows/src/test/java/com/r3/developers/samples/negotiation/workflows/NegotiationFlowTests.java @@ -0,0 +1,302 @@ +package com.r3.developers.samples.negotiation.workflows; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.r3.developers.samples.negotiation.workflows.accept.AcceptFlowArgs; +import com.r3.developers.samples.negotiation.workflows.accept.AcceptFlowRequest; +import com.r3.developers.samples.negotiation.workflows.modify.ModifyFlowArgs; +import com.r3.developers.samples.negotiation.workflows.modify.ModifyFlowRequest; +import com.r3.developers.samples.negotiation.workflows.propose.ProposalFlowArgs; +import com.r3.developers.samples.negotiation.workflows.propose.ProposalFlowRequest; +import net.corda.testing.driver.AllTestsDriver; +import net.corda.testing.driver.DriverNodes; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.application.flows.CordaInject; +import net.corda.virtualnode.VirtualNodeInfo; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class NegotiationFlowTests { + private static final Logger logger = LoggerFactory.getLogger(NegotiationFlowTests.class); + private static final MemberX500Name alice = MemberX500Name.parse("CN=Alice, OU=Application, O=R3, L=London, C=GB"); + private static final MemberX500Name bob = MemberX500Name.parse("CN=Bob, OU=Application, O=R3, L=London, C=GB"); + private static final MemberX500Name charles = MemberX500Name.parse("CN=Charles, OU=Application, O=R3, L=London, C=GB"); + private Map virtualNodes; + @CordaInject + UtxoLedgerService utxoLedgerService; + private static final ObjectMapper jsonMapper; + + static { + jsonMapper = new ObjectMapper(); + SimpleModule module = new SimpleModule(); + module.addSerializer(MemberX500Name.class, new MemberX500NameSerializer()); + module.addDeserializer(MemberX500Name.class, new MemberX500NameDeserializer()); + jsonMapper.registerModule(module); + } + + // Create the driver with Alice and Bob as participants, and Charlie as the notary + // This is to be created for all tests + @RegisterExtension + private final AllTestsDriver driver = + new DriverNodes(alice, bob).withNotary(charles, 1).forAllTests(); + + @BeforeAll + void setup() { + // Start the virtual nodes for Alice and Bob + virtualNodes = driver.let(dsl -> { + dsl.startNodes(Set.of(alice, bob)); + return dsl.nodesFor("workflows"); + }); + + assertThat(virtualNodes).withFailMessage("Failed to populate vNodes").isNotEmpty(); + } + + @Test + void test_that_ModifyProposalWithInvalidExistingProposalId_throws_exception() throws JsonProcessingException { + MemberX500Name responder = bob; + UUID fakeProposalId = UUID.randomUUID(); + // Create accept proposal flow using the fake proposal ID + String fakeModifyProposalFlowArgs = jsonMapper.writeValueAsString( + new ModifyFlowArgs(fakeProposalId, 30)); + + assertThatThrownBy(() -> { + // Execute the modify proposal flow with fake modify proposal arguments and Bob as the issuer + driver.run(dsl -> + dsl.runFlow(virtualNodes.get(responder), + ModifyFlowRequest.class, + () -> fakeModifyProposalFlowArgs) + ); + }).isInstanceOf(net.corda.testing.driver.node.FlowErrorException.class) + .hasMessageContaining("Multiple or zero Proposal states not found wth id:"); + } + + @Test + void test_that_ModifyProposalWithValidExistingProposalId_returns_newProposalId() throws JsonProcessingException { + MemberX500Name issuer = alice; + MemberX500Name responder = bob; + + // First issue a proposal + // Create proposal flow arguments with Bob as the responder + String proposalFlowArgs = jsonMapper.writeValueAsString( + new ProposalFlowArgs(20, responder, false)); + + // Execute the proposal flow using the proposal arguments created above with Alice as the issuer + String proposalId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(issuer), + ProposalFlowRequest.class, + () -> proposalFlowArgs) + ); + + // Secondly, modify the newly created proposal + // Create modify proposal flow arguments + String modifyProposalFlowArgs = jsonMapper.writeValueAsString( + new ModifyFlowArgs(UUID.fromString(proposalId), 30)); + + // Execute the modify proposal flow with Bob as the issuer + String newProposalId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(responder), + ModifyFlowRequest.class, + () -> modifyProposalFlowArgs) + ); + + // Assert that a trade was created (tradeId should be available) + assertThat(UUID.fromString(newProposalId)).isInstanceOf(UUID.class); + assertTrue(proposalId != newProposalId); + } + + @Test + void test_that_IssueByAliceModifyByBobAcceptByBob_throws_exception() throws JsonProcessingException { + MemberX500Name issuer = alice; + MemberX500Name responder = bob; + + // First Alice issues a proposal + // Create proposal flow arguments with Bob as the responder + String proposalFlowArgs = jsonMapper.writeValueAsString( + new ProposalFlowArgs(20, responder, false)); + + // Execute the proposal flow using the proposal arguments created above with Alice as the issuer + String proposalId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(issuer), + ProposalFlowRequest.class, + () -> proposalFlowArgs) + ); + + // Secondly, Bob modifies the proposal created by Alice + // Create modify proposal flow arguments + String modifyProposalFlowArgs = jsonMapper.writeValueAsString( + new ModifyFlowArgs(UUID.fromString(proposalId), 30)); + + // Execute the modify proposal flow with Bob as the issuer + String newProposalId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(responder), + ModifyFlowRequest.class, + () -> modifyProposalFlowArgs) + ); + + // Thirdly, Bob accepts the proposal that he modified, Alice did not get a say + // Create modify proposal flow arguments + String acceptProposalFlowArgs = jsonMapper.writeValueAsString( + new AcceptFlowArgs(UUID.fromString(newProposalId), responder)); + + assertThatThrownBy(() -> { + // Execute the accept proposal flow with Bob as the issuer or proposer + driver.run(dsl -> + dsl.runFlow(virtualNodes.get(responder), + AcceptFlowRequest.class, + () -> acceptProposalFlowArgs) + ); + }).isInstanceOf(net.corda.testing.driver.node.FlowErrorException.class) + .hasMessageContaining("The proposer cannot accept their own proposal"); + } + + @Test + void test_that_IssueByAliceModifyByBobAcceptByAlice_returns_tradeId() throws JsonProcessingException { + MemberX500Name issuer = alice; + MemberX500Name responder = bob; + + // First Alice issues a proposal + // Create proposal flow arguments with Bob as the responder + String proposalFlowArgs = jsonMapper.writeValueAsString( + new ProposalFlowArgs(20, responder, false)); + + // Execute the proposal flow using the proposal arguments created above with Alice as the issuer + String proposalId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(issuer), + ProposalFlowRequest.class, + () -> proposalFlowArgs) + ); + + // Secondly, Bob modifies the proposal created by Alice + // Create modify proposal flow arguments + String modifyProposalFlowArgs = jsonMapper.writeValueAsString( + new ModifyFlowArgs(UUID.fromString(proposalId), 30)); + + // Execute the modify proposal flow with Bob as the issuer + String newProposalId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(responder), + ModifyFlowRequest.class, + () -> modifyProposalFlowArgs) + ); + + // Thirdly, Bob accepts the proposal that he modified, Alice did not get a say + // Create modify proposal flow arguments + String acceptProposalFlowArgs = jsonMapper.writeValueAsString( + new AcceptFlowArgs(UUID.fromString(newProposalId), issuer)); + + // Execute the accept proposal flow with Bob as the issuer or proposer + String tradeId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(issuer), + AcceptFlowRequest.class, + () -> acceptProposalFlowArgs) + ); + + // Assert that a trade was created (tradeId should be available) + assertThat(UUID.fromString(tradeId)).isInstanceOf(UUID.class); + } + + @Test + void test_that_AcceptProposalWithInvalidProposalId_throws_exception() throws JsonProcessingException { + MemberX500Name responder = bob; + UUID fakeProposalId = UUID.randomUUID(); + // Create accept proposal flow using the fake proposal ID + String fakeAcceptProposalFlowArgs = jsonMapper.writeValueAsString( + new AcceptFlowArgs(fakeProposalId, responder)); + + assertThatThrownBy(() -> { + // Execute the accept proposal flow with fake proposal arguments and Bob as the issuer + driver.run(dsl -> + dsl.runFlow(virtualNodes.get(responder), + AcceptFlowRequest.class, + () -> fakeAcceptProposalFlowArgs) + ); + }).isInstanceOf(net.corda.testing.driver.node.FlowErrorException.class) + .hasMessageContaining("Multiple or zero Proposal states not found wth id:"); + } + + @Test + void test_that_AcceptProposalWithValidProposalId_returns_tradeId() throws JsonProcessingException { + MemberX500Name issuer = alice; + MemberX500Name responder = bob; + + // First issue a proposal + // Create proposal flow arguments with Bob as the responder + String proposalFlowArgs = jsonMapper.writeValueAsString( + new ProposalFlowArgs(20, responder, false)); + + // Execute the proposal flow using the proposal arguments created above with Alice as the issuer + String proposalId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(issuer), + ProposalFlowRequest.class, + () -> proposalFlowArgs) + ); + + // Secondly, accept the newly created proposal + // Create accept proposal flow arguments + String acceptProposalFlowArgs = jsonMapper.writeValueAsString( + new AcceptFlowArgs(UUID.fromString(proposalId), responder)); + + // Execute the accept proposal flow with Bob as the issuer + String tradeId = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(responder), + AcceptFlowRequest.class, + () -> acceptProposalFlowArgs) + ); + + // Assert that a trade was created (tradeId should be available) + assertThat(UUID.fromString(tradeId)).isInstanceOf(UUID.class); + } + + @Test + void test_that_IssueProposalWithIssuerAsBuyer_returns_proposalId() throws JsonProcessingException { + MemberX500Name issuer = alice; + MemberX500Name responder = bob; + + // Create proposal flow arguments with Bob as the responder + String proposalFlowArgs = jsonMapper.writeValueAsString( + new ProposalFlowArgs(20, responder, false)); + + // Execute the proposal flow with the proposal arguments created above and Alice as the issuer + String result = driver.let(dsl -> + dsl.runFlow(virtualNodes.get(issuer), + ProposalFlowRequest.class, + () -> proposalFlowArgs) + ); + // Check that a proposal was created (proposalId should be available) + assertThat(UUID.fromString(result)).isInstanceOf(UUID.class); + } + + @Test + void test_that_IssueProposalWithIssuerNotAsBuyer_throws_exception() throws JsonProcessingException { + MemberX500Name issuer = alice; + MemberX500Name responder = bob; + + // Create proposal flow arguments with Bob as the responder and the buyer + // (therefore, Alice who is the issuer is not the buyer, hence violating the smart contract) + String proposalFlowArgs = jsonMapper.writeValueAsString( + new ProposalFlowArgs(20, responder, true)); + + assertThatThrownBy(() -> { + // Run the proposal flow using the proposal arguments created above + driver.run(dsl -> + dsl.runFlow(virtualNodes.get(issuer), + ProposalFlowRequest.class, + () -> proposalFlowArgs)); + }).isInstanceOf(net.corda.testing.driver.node.FlowErrorException.class) + .hasMessageContaining("The buyer are the proposer"); + } +}