Signed-off-by: Ed Prosser <edward.prosser@r3.com>
19 KiB
Building your own CorDapp
After examining a functioning CorDapp, the next challenge is to create one of your own. We're going to build a simple supply chain CorDapp representing a network between a car dealership, a car manufacturer, and a bank.
To model this network, you need to create one state (representing cars), one contract (to control the rules governing cars), and one flow (to create cars). This CorDapp will be very basic, but entirely functional and deployable.
Step One: Download a template CorDapp
The first thing you need to do is clone a CorDapp template to modify.
- Open a terminal and navigate to a directory to store the new project.
- Run the following command to clone the template CorDapp:
git clone https://github.com/corda/cordapp-template-kotlin.git
- Open IntelliJ and open the CorDapp template project.
Step Two: Creating states
Since the CorDapp models a car dealership network, a state must be created to represent cars. States are immutable objects representing on-ledger facts. A state might represent a physical asset like a car, or an intangible asset or agreement like an IOU. For more information on states, see the state documentation.
- From IntelliJ expand the source files and navigate to
contracts > src > main > kotlin > com.template > states > TemplateState.kt
.
This file contains a skeleton state definition.
- Right-click on TemplateState.kt in the project navigation on the left. Select Refactor > Copy.
- Rename the file to
CarState
and click OK.
- 4. Double-click the new state file to open it. Add the following fields to the state:
owningBank
of typeParty
holdingDealer
of typeParty
manufacturer
of typeParty
vin
of typeString
licensePlateNumber
of typeString
make
of typeString
model
of typeString
dealershipLocation
of typeString
linearId
of typeUniqueIdentifier
Don't worry if you're not sure exactly how these should appear, you can check your code shortly.
Remove the
data
andparticipants
parameters.Add a body to the
CarState
class that overrides participants to contain a list ofowningBank
,holdingDealer
, andmanufacturer
.The
CarState
file should now appear as follows:@BelongsToContract(TemplateContract::class) data class CarState(val owningbank: Party, val holdingDealer: Party, val manufacturer: Party, val vin: String, val licensePlateNumber: String, val make: String, val model: String, val dealershipLocation: String, val linearId: UniqueIdentifier) : ContractState { override val participants: List<AbstractParty> = listOf(owningBank, holdingDealer, manufacturer) }
Save the
CarState.kt
file.
The CarState
definition has now been created. It lists the properties and associated types required of all instances of this state.
Step Three: Creating contracts
After creating a state, you must create a contract to dictate how the state can operate.
- From IntelliJ, expand the project source and navigate to:
contracts > src > main > kotlin > com.template > contracts > TemplateContract.kt
- Right-click on TemplateContract.kt in the project navigation on the left. Select Refactor > Copy.
- Rename the file to
CarContract
and click OK. - Double-click the new contract file to open it.
- Update the ID field to
com.template.contracts.CarContract
. This ID field is used to identify contracts when building a transaction. - Update the
Action
command to anIssue
command. This represents an issuance of an instance of theCarState
state. - Add
val command=tx.commands.requireSingleCommand<Commands.Issue>()
at the beginning of theverify()
method. This line ensures that the command to issue a car state is called. - The final function of the contract is to prevent unwanted behaviour during the flow. Add the following requirements to the contract:
- There should be no input state to the transaction.
- There should be one output state.
- The output state must be of the type
CarState
.- The
licensePlateNumber
must be seven characters long.
The
CarContract.kt
file should look as follows:class CarContract : Contract { companion object { val ID = "com.template.contracts.CarContract" const } override fun verify(tx: LedgerTransaction) { val command = tx.commands.requireSingleCommand<Commands.Issue>() requireThat {"There should be no input state" using (tx.inputs.isEmpty()) "There should be one input state" using (tx.outputs.size == 1) "The output state must be of type CarState" using (tx.outputs.get(0).data is CarState) val outputState = tx.outputs.get(0).data as CarState "The licensePlateNumber must be seven characters long" using (outputState.licensePlateNumber.length == 7) } } interface Commands : CommandData { class Issue : Commands } }
Save the
CarContract.kt
file.
Step Four: Creating a flow
- From IntelliJ, expand the project source and navigate to:
contracts > src > main > kotlin > com.template > contracts > Flows.kt
- Right-click on Flows.kt in the project navigation on the left. Select Refactor > Copy.
- Rename the file to
CarFlow
and click OK. - Double-click the new contract file to open it.
- Update the name of the
Initiator
class toCarIssueInitiator
. - Update the name of the
Responder
class toCarIssueResponder
. - Update the
@InitiatedBy
property ofCarIssueResponder
toCarIssueInitiator::class
. - Add parameters to the
CarIssueInitiator
class for all the fields of theCarState
definition, except forlinearId
. - Inside the
call()
function of the initiator, create a variable for the notary node. expand this with some code - Create a variable for an
Issue
command.
The first parameter of the command must be the command type, in this case
Issue
.The second parameter of the command must be a list of keys from the relevant parties, in this case
owningBank
,holdingDealer
, andmanufacturer
.
- Create a
CarState
object using the parameters ofCarIssueInitiator
.
The last parameter for
CarState
must be a newUniqueIdentifier()
object.
The
CarFlow.kt
file should look like this:@InitiatingFlow @StartableByRPC class CarIssueInitiator(val owningBank: Party, val holdingDealer: Party, val manufacturer: Party, val vin: String, val licensePlateNumber: String, val make: String, val model: String, val dealershipLocation: String) : FlowLogic<Unit>() { override val progressTracker = ProgressTracker() @Suspendable override fun call() { val notary = serviceHub.networkMapCache.notaryIdentities.single() val command = Command(CarContract.Commands.Issue(), listOf(owningBank, holdingDealer, manufacturer).map { it.owningKey }) val carState = CarState(owningBank, holdingDealer, manufacturer, vin, licensePlateNumber, make, model, dealershipLocation, UniqueIdentifier()) } } @InitiatedBy(CarIssueInitiator::class) class CarIssueResponder(val counterpartySession: FlowSession) : FlowLogic<Unit>() { @Suspendable override fun call(){ } } }
So far you've...
Next you must...
Update the
FlowLogic<Unit>
toFlowLogic<SignedTransaction>
in both the initiator and responder class.Update the return type of both
call()
transactions to be of typeSignedTransaction
.In the
call()
function, create aTransactionBuilder
object similarly. TheTransactionBuilder
class should take in the notary node. The output state and command must be added to theTransactionBuilder
.Verify the transaction by calling
verify(serviceHub)
on theTransactionBuilder
.Sign the transaction and store the result in a variable.
Delete the
progressTracker
as it won't be used in this tutorial.The
CarFlow.kt
file should now look like this:@InitiatingFlow @StartableByRPC class CarIssueInitiator(val owningBank: Party, val holdingDealer: Party, val manufacturer: Party, val vin: String, val licensePlateNumber: String, val make: String, val model: String, val dealershipLocation: String) : FlowLogic<SignedTransaction>() { override val progressTracker = ProgressTracker() @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryIdentities.single() val command = Command(CarContract.Commands.Issue(), listOf(owningBank, holdingDealer, manufacturer).map { it.owningKey }) val carState = CarState(owningBank, holdingDealer, manufacturer, vin, licensePlateNumber, make, model, dealershipLocation, UniqueIdentifier()) val txBuilder = TransactionBuilder(notary) .addOutputState(carState, CarContract.ID) .addCommand(command) txBuilder.verify(serviceHub)val tx = serviceHub.signInitialTransaction(txBuilder) } } @InitiatedBy(CarIssueInitiator::class) class CarIssueResponder(val counterpartySession: FlowSession) : FlowLogic<SignedTransaction>() { @Suspendable override fun call(): SignedTransaction { } } }
So far you've...
Next you must...
To finish the initiators
call()
function, other parties must sign the transaction. Add the following code to send the transaction to the other relevant parties:val sessions = (carState.participants - ourIdentity).map { initiateFlow(it as Party) } val stx = subFlow(CollectSignaturesFlow(tx, sessions)) return subFlow(FinalityFlow(stx, sessions))
The first line creates a
List<FlowSession>
object by callinginitiateFlow()
for each party. The second line collects signatures from the relevant parties and returns a signed transaction. The third line callsFinalityFlow()
, finalizes the transaction using the notary or notary pool.
Lastly, the body of the responder flow must be completed. The following code checks the transaction contents, signs it, and sends it back to the initiator:
@Suspendable override fun call(): SignedTransaction { val signedTransactionFlow = object : SignTransactionFlow(counterpartySession) { override fun checkTransaction(stx: SignedTransaction) = requireThat { val output = stx.tx.outputs.single().data "The output must be a CarState" using (output is CarState) } }val txWeJustSignedId = subFlow(signedTransactionFlow) return subFlow(ReceiveFinalityFlow(counterpartySession, txWeJustSignedId.id)) }
The completed
CarFlow.kt
should look like this:@InitiatingFlow @StartableByRPC class CarIssueInitiator(val owningBank: Party, val holdingDealer: Party, val manufacturer: Party, val vin: String, val licensePlateNumber: String, val make: String, val model: String, val dealershipLocation: String) : FlowLogic<SignedTransaction>() { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryIdentities.single() val command = Command(CarContract.Commands.Issue(), listOf(owningBank, holdingDealer, manufacturer).map { it.owningKey }) val carState = CarState(owningBank, holdingDealer, manufacturer, vin, licensePlateNumber, make, model, dealershipLocation, UniqueIdentifier()) val txBuilder = TransactionBuilder(notary) .addOutputState(carState, CarContract.ID) .addCommand(command) txBuilder.verify(serviceHub)val tx = serviceHub.signInitialTransaction(txBuilder) val sessions = (carState.participants - ourIdentity).map { initiateFlow(it as Party) } val stx = subFlow(CollectSignaturesFlow(tx, sessions)) return subFlow(FinalityFlow(stx, sessions)) } } @InitiatedBy(CarIssueInitiator::class) class CarIssueResponder(val counterpartySession: FlowSession) : FlowLogic<SignedTransaction>() { @Suspendable override fun call(): SignedTransaction { val signedTransactionFlow = object : SignTransactionFlow(counterpartySession) { override fun checkTransaction(stx: SignedTransaction) = requireThat { val output = stx.tx.outputs.single().data "The output must be a CarState" using (output is CarState) } }val txWeJustSignedId = subFlow(signedTransactionFlow) return subFlow(ReceiveFinalityFlow(counterpartySession, txWeJustSignedId.id)) } }
Step Five: Update the Gradle build
The Gradle build files must be updated to change how the nodes are deployed. (how)
Navigate to the
build.gradle
file in the rootcordapp-template-kotlin
directory.In the
deployNodes
task, update the nodes to read as follows:node {"O=Notary,L=London,C=GB" name false] notary = [validating : 10002 p2pPort rpcSettings {"localhost:10003") address("localhost:10043") adminAddress( } } node {"O=Dealership,L=London,C=GB" name 10005 p2pPort rpcSettings {"localhost:10006") address("localhost:10046") adminAddress( }"user1", "password": "test", "permissions": ["ALL"]]] rpcUsers = [[ user: } node {"O=Manufacturer,L=New York,C=US" name 10008 p2pPort rpcSettings {"localhost:10009") address("localhost:10049") adminAddress( }"user1", "password": "test", "permissions": ["ALL"]]] rpcUsers = [[ user:: } node {"O=BankofAmerica,L=New York,C=US" name 10010 p2pPort rpcSettings {"localhost:10007") address("localhost:10047") adminAddress( }"user1", "password": "test", "permissions": ["ALL"]]] rpcUsers = [[ user: }
Save the updated
build.gradle
file and click Import Changes when the pop-up message appears in the lower-right corner.
Step Six: Deploying your CorDapp locally
Now that the the CorDapp code has been completed and the build file updated, the CorDapp can be deployed.
- Open a terminal and navigate to the root directory of the project.
- Run
./gradlew clean deployNodes
- Run
build/nodes/runNodes
- To run flows in your CorDapp, enter the following flow command from any node terminal window:
flow start CarIssueInitiator owningBank: Bank of America, holdingDealer: Dealership, manufacturer: Manufacturer, vin:"abc", licensePlateNumber: "abc1234", make: "Honda", model: "Civic", dealershipLocation: "NYC"
- To check that the state was correctly issued, query the node using the following command:
run vaultQuery contractStateType: com.template.states.CarState
The vault is the node's repository of all information from the ledger that involves that node, stored in a relational model. After running the query, the terminal should display the state created by the flow command. This command can be run from the terminal window of any node, as all parties are participants in this transaction.
Next steps
The getting started experience is designed to be lightweight and get to code as quickly as possible, for more detail, see the following documentation:
For operational users, see the following documentation: