Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core-16349: corda 5 Kotlin negotiation app #21

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions kotlin-samples/corda5-negotiation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Negotiation Cordapp

This CorDapp shows how multi-party negotiation is handled on the Corda ledger, in the absence of an API for user
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
* Modifying the proposal, consuming the existing `ProposalState` and replacing it with a new `ProposalState` for a new
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.

### Flows

We start with the proposal flow implemented in `ProposalFlow.kt`.


The modification of the proposal is implemented in `ModificationFlow.kt`.


In the `AcceptanceFlow.kt`, we receive the modified ProposalState and it's 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

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.

#### 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.).

Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body:
```
{
"clientRequestId": "createProposal",
"flowClassName": "com.r3.developers.samples.negotiation.workflows.Propose.ProposalFlowRequest",
"requestBody": {
"amount": 20,
"counterParty":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"
}
}
```
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.


#### 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:
```
{
"clientRequestId": "list-1",
"flowClassName": "com.r3.developers.samples.negotiation.workflows.util.ListProposal",
"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.


#### 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:
```
{
"clientRequestId": "ModifyFlow",
"flowClassName": "com.r3.developers.samples.negotiation.workflows.Modify.ModifyFlowRequest",
"requestBody": {
"newAmount": 22,
"proposalID": "<use the proposal id here>"
}
}
```
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.
```
{
"clientRequestId": "AcceptFlow",
"flowClassName": "com.r3.developers.samples.negotiation.workflows.Accept.AcceptFlowRequest",
"requestBody": {
"proposalID": "<use the proposal id here>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing the variable acceptor, but why do you need this input?
The accepter can only be the proposee of the proposal. In the contract, you can just use the proposee variable.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @peterli-r3 I've added the Acceptor to the documentation. We still need to know who is trying to accept the trade to be able to perform the necessary checks in the Accept contract.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @khutsijabari , I had quickly chatted with @7Dhruv eariler, You dont really need to add a variable to keep track of the proposer. You can check it in two ways:

  1. Check if the proposer with myInfo in the AcceptProposal flow initiator.
  2. Make the accept command take input, we can pass in the myInfo into the contract via the command.
    The idea is that we will try to have as less variables as possible in the states.

}
}
```
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.

### App Diagrams
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)





84 changes: 84 additions & 0 deletions kotlin-samples/corda5-negotiation/contracts/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@

plugins {
// Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well.
// These extend existing build environment so that CPB and CPK files can be built.
// This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp
// required by Corda.
id 'net.corda.plugins.cordapp-cpb2'
id 'org.jetbrains.kotlin.jvm'
id 'maven-publish'
}

// Declare dependencies for the modules we will use.
// A cordaProvided declaration is required for anything that we use that the Corda API provides.
// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on.
dependencies {

cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle'

// Declare a "platform" so that we use the correct set of dependency versions for the version of the
// Corda API specified.
cordaProvided platform("net.corda:corda-api:$cordaApiVersion")

// If using transistive dependencies this will provide most of Corda-API:
// cordaProvided 'net.corda:corda-application'

// Alternatively we can explicitly specify all our Corda-API dependencies:
cordaProvided 'net.corda:corda-base'
cordaProvided 'net.corda:corda-serialization'
cordaProvided 'net.corda:corda-ledger-utxo'
cordaProvided 'net.corda:corda-ledger-consensual'

// CorDapps that use the UTXO ledger must include at least one notary client plugin
cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion"

// The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration.
cordaProvided 'org.slf4j:slf4j-api'

// This are shared so should be here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block should probably be deleted

// Dependencies Required By Test Tooling
// Todo: these are commented out as the simulator UTXO work has not been merged into Gecko yet.
// testImplementation "net.corda:corda-simulator-api:$simulatorVersion"
// testRuntimeOnly "net.corda:corda-simulator-runtime:$simulatorVersion"

// 3rd party libraries
// Required
testImplementation "org.slf4j:slf4j-simple:2.0.0"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"

// Optional but used by exmaple tests.
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion"
}

// The CordApp section.
// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp.
// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s
// subproject.
// This is required by the corda plugins to build the CorDapp.
cordapp {
// "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred
// and earliest versions of the Corda platform that the CorDapp will run on respectively.
// Enforced versioning has not implemented yet so we need to pass in a dummy value for now.
// The platform version will correspond to and be roughly equivalent to the Corda API version.
targetPlatformVersion = platformVersion.toInteger()
minimumPlatformVersion = platformVersion.toInteger()

// The cordapp section contains either a workflow or contract subsection depending on the type of component.
// Declares the type and metadata of the CPK (this CPB has one CPK).
contract {
name "ContractsModuleNameHere"
versionId 1
vendor "VendorNameHere"
}
}

publishing {
publications {
maven(MavenPublication) {
from components.cordapp
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.r3.developers.samples.negotiation

import com.r3.developers.samples.negotiation.util.Member
import net.corda.v5.ledger.utxo.BelongsToContract
import net.corda.v5.ledger.utxo.ContractState
import java.security.PublicKey
import java.util.*

@BelongsToContract(ProposalAndTradeContract::class)
class Proposal(
var amount: Int,

var buyer: Member,

var seller: Member,

var proposer: Member,

var proposee: Member,

val proposalID: UUID,
) : ContractState {
override fun getParticipants(): List<PublicKey> {
return listOf(proposer.ledgerKey, proposee.ledgerKey)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.r3.developers.samples.negotiation

import net.corda.v5.base.exceptions.CordaRuntimeException
import net.corda.v5.ledger.utxo.Command
import net.corda.v5.ledger.utxo.Contract
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction

class ProposalAndTradeContract : Contract {
override fun verify(transaction: UtxoLedgerTransaction) {
// Extract the command from the transaction
// Verify the transaction according to the intention of the transaction
val command = transaction.commands[0] as NegotiationCommands
command.verify(transaction)
}

interface NegotiationCommands : Command {
fun verify(transaction: UtxoLedgerTransaction?)
}

class Propose : NegotiationCommands {
var noInputsMsg = "There are no inputs"
var oneOutputMsg = "Only one output state should be created."
var outputTypeMsg = "The single output is of type ProposalState"
var commandMsg = "There is exactly one command"
var buyerMsg = "The buyer are the proposer"
var sellerMsg = "The seller are the proposee"
var proposerMsg = "The proposer is a required signer"
var proposeeMsg = "The proposee is a required signer"

override fun verify(transaction: UtxoLedgerTransaction?) {
val proposalStateOutput = transaction!!.getOutputStates(Proposal::class.java)[0]

require(transaction.inputStateAndRefs.isEmpty(), noInputsMsg)
require(transaction.outputTransactionStates.size == 1, oneOutputMsg)
require(transaction.getOutputStates(Proposal::class.java).size == 1, outputTypeMsg)
require(transaction.commands.size == 1, commandMsg)
require(proposalStateOutput.proposer.toString() == proposalStateOutput.buyer.toString(), buyerMsg)
require(proposalStateOutput.proposee.toString() == proposalStateOutput.seller.toString(), sellerMsg)
require(transaction.signatories.contains(proposalStateOutput.proposer.ledgerKey), proposerMsg)
require(transaction.signatories.contains(proposalStateOutput.proposee.ledgerKey), proposeeMsg)
}
}

class Modify : NegotiationCommands {
var oneInputMsg = "There is exactly one input"
var inputTypeMsg = "The single input is of type ProposalState"
var oneOutputMsg = "There is exactly one output"
var outputTypeMsg = "The single output is of type ProposalState"
var oneCommandMsg = "There is exactly one command"
var amountModifiedMsg = "The amount is modified in the output"
var buyerMsg = "The buyer is unmodified in the output"
var sellerMsg = "The seller is unmodified in the output"
var proposerMsg = "The proposer is a required signer"
var proposeeMsg = "The proposee is a required signer"
override fun verify(transaction: UtxoLedgerTransaction?) {
val proposalStateOutput = transaction!!.getOutputStates(Proposal::class.java)[0]
val proposalStateInputs = transaction.getInputStates(Proposal::class.java)[0]

require(transaction.inputStateAndRefs.size == 1, oneInputMsg)
require(transaction.getInputStates(Proposal::class.java).size == 1, inputTypeMsg)
require(transaction.outputTransactionStates.size == 1, oneOutputMsg)
require(transaction.getOutputStates(Proposal::class.java).size == 1, outputTypeMsg)
require(transaction.commands.size == 1, oneCommandMsg)
require(proposalStateOutput.amount != proposalStateInputs.amount, amountModifiedMsg)
require(proposalStateInputs.buyer.toString() == proposalStateOutput.buyer.toString(), buyerMsg)
require(proposalStateInputs.seller.toString() == proposalStateOutput.seller.toString(), sellerMsg)

require(transaction.signatories.contains(proposalStateInputs.proposer.ledgerKey), proposerMsg)
require(transaction.signatories.contains(proposalStateInputs.proposee.ledgerKey), proposeeMsg)
}
}

class Accept : NegotiationCommands {

var oneInputMsg = "There is exactly one input"
var inputTypeMsg = "The single input is of type ProposalState"
var oneOutputMsg = "There is exactly one output"
var outputTypeMsg = "The single output is of type TradeState"
var oneCommandMsg = "There is exactly one command"
var amountMsg = "The amount is unmodified in the output"
var buyerMsg = "The buyer is unmodified in the output"
var sellerMsg = "The seller is unmodified in the output"
var proposerMsg = "The proposer is a required signer"
var proposeMsg = "The propose is a required signer"
override fun verify(transaction: UtxoLedgerTransaction?) {
val tradeStateOutput = transaction!!.getOutputStates(Trade::class.java)[0]
val proposalStateInputs = transaction.getInputStates(Proposal::class.java)[0]

require(transaction.inputStateAndRefs.size == 1, oneInputMsg)
require(transaction.getInputStates(Proposal::class.java).size == 1, inputTypeMsg)
require(transaction.outputTransactionStates.size == 1, oneOutputMsg)
require(transaction.getOutputStates(Trade::class.java).size == 1, outputTypeMsg)
require(transaction.commands.size == 1, oneCommandMsg)
require(tradeStateOutput.amount == proposalStateInputs.amount, amountMsg)
require(proposalStateInputs.buyer.toString() == tradeStateOutput.buyer.toString(), buyerMsg)
require(proposalStateInputs.seller.toString() == tradeStateOutput.seller.toString(), sellerMsg)
require(transaction.signatories.contains(proposalStateInputs.proposer.ledgerKey), proposerMsg)
require(
transaction.signatories.contains(proposalStateInputs.proposee.ledgerKey), proposeMsg
)
}
}


companion object {
private fun require(asserted: Boolean, errorMessage: String) {
if (!asserted) {
throw CordaRuntimeException(errorMessage)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.r3.developers.samples.negotiation

import com.r3.developers.samples.negotiation.util.Member
import net.corda.v5.ledger.utxo.BelongsToContract
import net.corda.v5.ledger.utxo.ContractState
import java.security.PublicKey
import java.util.*

@BelongsToContract(ProposalAndTradeContract::class)
class Trade(
var amount: Int,

var buyer: Member,

var seller: Member,

private val participants: List<PublicKey>
) : ContractState {

override fun getParticipants(): List<PublicKey> {
return participants
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.r3.developers.samples.negotiation.util

import net.corda.v5.base.annotations.CordaSerializable
import net.corda.v5.base.types.MemberX500Name
import java.security.PublicKey

/**
* This class encompasses a participants X500 name and its key. This is used in the contract to conveniently
* get the key corresponding to a participant, so it can be checked for required signatures.
*/
@CordaSerializable
class Member(
val name: MemberX500Name,
val ledgerKey: PublicKey
) {
override fun toString(): String {
return "Member{name=$name}"
}
}
Loading