Enterprise Blockchain Development with Corda CorDapp in Java

Building Distributed Business Applications on Corda Platform


Article

Corda is an open-source blockchain platform designed specifically for business applications. Unlike traditional blockchains, Corda focuses on privacy, scalability, and legal enforceability. CorDapps (Corda Distributed Applications) are the building blocks that run on Corda networks, enabling businesses to transact directly with strong privacy guarantees.

Corda Architecture Overview

Key Concepts:

  • States: Shared facts on the ledger
  • Contracts: Rules governing state evolution
  • Flows: Business processes that coordinate updates
  • Transactions: Proposed updates to the ledger
  • Nodes: Network participants running Corda
  • Notary: Prevents double-spends and provides finality

Corda vs Traditional Blockchains:

  • No Global Broadcast: Only relevant parties see transactions
  • Legal Identity: Real-world legal entities on the network
  • Pluggable Consensus: Flexible consensus mechanisms
  • No Native Cryptocurrency: Focus on business assets

1. Project Setup and Dependencies

Gradle Configuration (build.gradle):

plugins {
id 'java'
id 'net.corda.plugins.cordapp' version '6.0.0'
id 'net.corda.plugins.quasar-utils' version '6.0.0'
}
repositories {
mavenCentral()
maven { url 'https://software.r3.com/artifactory/corda' }
}
cordapp {
targetPlatformVersion 6
minimumPlatformVersion 6
contract {
name "Trade Finance CorDapp"
versionId 1
vendor "My Company"
licence "Apache 2"
}
workflow {
name "Trade Finance CorDapp"
versionId 1
vendor "My Company"
licence "Apache 2"
}
}
dependencies {
// Corda dependencies
cordaCompile "net.corda:corda-core:4.9"
cordaCompile "net.corda:corda-node-api:4.9"
cordaRuntime "net.corda:corda:4.9"
// CorDapp dependencies
cordapp "net.corda:corda-finance-contracts:4.9"
cordapp "net.corda:corda-finance-workflows:4.9"
// Testing
testCompile "net.corda:corda-node-driver:4.9"
testCompile "junit:junit:4.13.2"
// Quasar for flow checkpoints
cordaCompile "co.paralleluniverse:quasar-core:0.7.15"
}
tasks.withType(JavaCompile) {
options.compilerArgs << "-parameters"
}

2. State Definition

States represent shared facts on the ledger. Let's create a trade finance state:

package com.tradefinance.states;
import net.corda.core.contracts.BelongsToContract;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.LinearState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.Party;
import net.corda.core.serialization.CordaSerializable;
import java.util.Arrays;
import java.util.List;
/**
* Letter of Credit State representing a trade finance instrument
*/
@BelongsToContract(LetterOfCreditContract.class)
public class LetterOfCreditState implements LinearState {
private final UniqueIdentifier linearId;
private final Party applicant;       // Buyer (importer)
private final Party beneficiary;     // Seller (exporter)
private final Party issuingBank;     // Issuing bank
private final Party advisingBank;    // Advising bank (optional)
private final String lcNumber;
private final Double amount;
private final String currency;
private final String goodsDescription;
private final String shippingTerms;
private final LCStatus status;
private final java.time.Instant issueDate;
private final java.time.Instant expiryDate;
public LetterOfCreditState(UniqueIdentifier linearId,
Party applicant,
Party beneficiary,
Party issuingBank,
Party advisingBank,
String lcNumber,
Double amount,
String currency,
String goodsDescription,
String shippingTerms,
LCStatus status,
java.time.Instant issueDate,
java.time.Instant expiryDate) {
this.linearId = linearId;
this.applicant = applicant;
this.beneficiary = beneficiary;
this.issuingBank = issuingBank;
this.advisingBank = advisingBank;
this.lcNumber = lcNumber;
this.amount = amount;
this.currency = currency;
this.goodsDescription = goodsDescription;
this.shippingTerms = shippingTerms;
this.status = status;
this.issueDate = issueDate;
this.expiryDate = expiryDate;
}
// Getters
@Override
public UniqueIdentifier getLinearId() { return linearId; }
public Party getApplicant() { return applicant; }
public Party getBeneficiary() { return beneficiary; }
public Party getIssuingBank() { return issuingBank; }
public Party getAdvisingBank() { return advisingBank; }
public String getLcNumber() { return lcNumber; }
public Double getAmount() { return amount; }
public String getCurrency() { return currency; }
public String getGoodsDescription() { return goodsDescription; }
public String getShippingTerms() { return shippingTerms; }
public LCStatus getStatus() { return status; }
public java.time.Instant getIssueDate() { return issueDate; }
public java.time.Instant getExpiryDate() { return expiryDate; }
@Override
public List<AbstractParty> getParticipants() {
if (advisingBank != null) {
return Arrays.asList(applicant, beneficiary, issuingBank, advisingBank);
}
return Arrays.asList(applicant, beneficiary, issuingBank);
}
// State evolution methods
public LetterOfCreditState withStatus(LCStatus newStatus) {
return new LetterOfCreditState(
linearId, applicant, beneficiary, issuingBank, advisingBank,
lcNumber, amount, currency, goodsDescription, shippingTerms,
newStatus, issueDate, expiryDate
);
}
public LetterOfCreditState withAdvisingBank(Party newAdvisingBank) {
return new LetterOfCreditState(
linearId, applicant, beneficiary, issuingBank, newAdvisingBank,
lcNumber, amount, currency, goodsDescription, shippingTerms,
status, issueDate, expiryDate
);
}
// Status enumeration
@CordaSerializable
public enum LCStatus {
ISSUED,
ADVISED,
AMENDED,
UTILIZED,
ACCEPTED,
EXPIRED,
CANCELLED
}
}

3. Contract Definition

Contracts define the rules for state evolution:

package com.tradefinance.contracts;
import com.tradefinance.states.LetterOfCreditState;
import net.corda.core.contracts.Command;
import net.corda.core.contracts.CommandData;
import net.corda.core.contracts.Contract;
import net.corda.core.contracts.ContractState;
import net.corda.core.identity.Party;
import net.corda.core.transactions.LedgerTransaction;
import java.security.PublicKey;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static net.corda.core.contracts.ContractsDSL.requireThat;
/**
* Contract for Letter of Credit transactions
*/
public class LetterOfCreditContract implements Contract {
public static final String ID = "com.tradefinance.contracts.LetterOfCreditContract";
@Override
public void verify(LedgerTransaction tx) {
// Extract commands
final List<Command<CommandData>> commands = tx.getCommands();
for (Command<?> command : commands) {
final CommandData commandData = command.getValue();
if (commandData instanceof Commands.Issue) {
verifyIssue(tx, command);
} else if (commandData instanceof Commands.Advise) {
verifyAdvise(tx, command);
} else if (commandData instanceof Commands.Amend) {
verifyAmend(tx, command);
} else if (commandData instanceof Commands.Utilize) {
verifyUtilize(tx, command);
} else if (commandData instanceof Commands.Accept) {
verifyAccept(tx, command);
} else {
throw new IllegalArgumentException("Unrecognized command");
}
}
}
private void verifyIssue(LedgerTransaction tx, Command<?> command) {
requireThat(require -> {
// Structural requirements
require.using("No inputs should be consumed when issuing an LC", 
tx.getInputStates().isEmpty());
require.using("Only one LC state should be created", 
tx.getOutputStates().size() == 1);
// Output state requirements
ContractState outputState = tx.getOutputStates().get(0);
require.using("Output must be a LetterOfCreditState", 
outputState instanceof LetterOfCreditState);
LetterOfCreditState lcState = (LetterOfCreditState) outputState;
// Business rules
require.using("LC amount must be positive", 
lcState.getAmount() > 0);
require.using("Issue date must be before expiry date", 
lcState.getIssueDate().isBefore(lcState.getExpiryDate()));
require.using("LC must be in ISSUED status", 
lcState.getStatus() == LetterOfCreditState.LCStatus.ISSUED);
// Signer requirements
Set<PublicKey> signers = new HashSet<>(command.getSigners());
Set<PublicKey> expectedSigners = getExpectedSigners(lcState);
require.using("Applicant and issuing bank must sign issue transaction",
signers.containsAll(expectedSigners));
return null;
});
}
private void verifyAdvise(LedgerTransaction tx, Command<?> command) {
requireThat(require -> {
// Structural requirements
require.using("Advise transaction must have exactly one input and one output",
tx.getInputStates().size() == 1 && tx.getOutputStates().size() == 1);
// State type requirements
ContractState inputState = tx.getInputStates().get(0);
ContractState outputState = tx.getOutputStates().get(0);
require.using("Input must be a LetterOfCreditState", 
inputState instanceof LetterOfCreditState);
require.using("Output must be a LetterOfCreditState", 
outputState instanceof LetterOfCreditState);
LetterOfCreditState inputLC = (LetterOfCreditState) inputState;
LetterOfCreditState outputLC = (LetterOfCreditState) outputState;
// Business rules for advising
require.using("Input LC must be in ISSUED status",
inputLC.getStatus() == LetterOfCreditState.LCStatus.ISSUED);
require.using("Output LC must be in ADVISED status",
outputLC.getStatus() == LetterOfCreditState.LCStatus.ADVISED);
require.using("Only status should change during advise",
statesEqualExceptStatus(inputLC, outputLC));
// Signer requirements
Set<PublicKey> signers = new HashSet<>(command.getSigners());
require.using("Advising bank must sign advise transaction",
signers.contains(outputLC.getAdvisingBank().getOwningKey()));
return null;
});
}
private void verifyAmend(LedgerTransaction tx, Command<?> command) {
requireThat(require -> {
// Similar verification logic for amendments
require.using("Amend transaction must have exactly one input and one output",
tx.getInputStates().size() == 1 && tx.getOutputStates().size() == 1);
LetterOfCreditState inputLC = (LetterOfCreditState) tx.getInputStates().get(0);
LetterOfCreditState outputLC = (LetterOfCreditState) tx.getOutputStates().get(0);
require.using("All parties must agree to amendment",
command.getSigners().containsAll(getAllParticipantKeys(inputLC)));
return null;
});
}
private void verifyUtilize(LedgerTransaction tx, Command<?> command) {
requireThat(require -> {
// Utilization verification logic
require.using("Utilize transaction must have exactly one input and one output",
tx.getInputStates().size() == 1 && tx.getOutputStates().size() == 1);
LetterOfCreditState inputLC = (LetterOfCreditState) tx.getInputStates().get(0);
LetterOfCreditState outputLC = (LetterOfCreditState) tx.getOutputStates().get(0);
require.using("LC must not be expired for utilization",
java.time.Instant.now().isBefore(inputLC.getExpiryDate()));
require.using("Beneficiary must sign utilization",
command.getSigners().contains(inputLC.getBeneficiary().getOwningKey()));
return null;
});
}
private void verifyAccept(LedgerTransaction tx, Command<?> command) {
requireThat(require -> {
// Acceptance verification logic
require.using("Accept transaction must have exactly one input and one output",
tx.getInputStates().size() == 1 && tx.getOutputStates().size() == 1);
LetterOfCreditState inputLC = (LetterOfCreditState) tx.getInputStates().get(0);
LetterOfCreditState outputLC = (LetterOfCreditState) tx.getOutputStates().get(0);
require.using("Applicant must sign acceptance",
command.getSigners().contains(inputLC.getApplicant().getOwningKey()));
return null;
});
}
// Helper methods
private boolean statesEqualExceptStatus(LetterOfCreditState state1, LetterOfCreditState state2) {
return state1.getLinearId().equals(state2.getLinearId()) &&
state1.getApplicant().equals(state2.getApplicant()) &&
state1.getBeneficiary().equals(state2.getBeneficiary()) &&
state1.getIssuingBank().equals(state2.getIssuingBank()) &&
state1.getAmount().equals(state2.getAmount()) &&
state1.getCurrency().equals(state2.getCurrency());
}
private Set<PublicKey> getExpectedSigners(LetterOfCreditState state) {
Set<PublicKey> expected = new HashSet<>();
expected.add(state.getApplicant().getOwningKey());
expected.add(state.getIssuingBank().getOwningKey());
return expected;
}
private Set<PublicKey> getAllParticipantKeys(LetterOfCreditState state) {
return state.getParticipants().stream()
.map(AbstractParty::getOwningKey)
.collect(Collectors.toSet());
}
// Command definitions
public interface Commands extends CommandData {
class Issue implements Commands {}
class Advise implements Commands {}
class Amend implements Commands {}
class Utilize implements Commands {}
class Accept implements Commands {}
class Expire implements Commands {}
}
}

4. Flow Development

Flows coordinate the business processes between parties:

Issue Letter of Credit Flow:

package com.tradefinance.flows;
import co.paralleluniverse.fibers.Suspendable;
import com.tradefinance.contracts.LetterOfCreditContract;
import com.tradefinance.states.LetterOfCreditState;
import net.corda.core.contracts.Command;
import net.corda.core.contracts.StateAndRef;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.flows.*;
import net.corda.core.identity.Party;
import net.corda.core.node.services.Vault;
import net.corda.core.node.services.vault.QueryCriteria;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.transactions.TransactionBuilder;
import net.corda.core.utilities.ProgressTracker;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
/**
* Flow for issuing a new Letter of Credit
*/
@InitiatingFlow
@StartableByRPC
public class IssueLetterOfCreditFlow extends FlowLogic<SignedTransaction> {
private final Party beneficiary;
private final Party issuingBank;
private final String lcNumber;
private final Double amount;
private final String currency;
private final String goodsDescription;
private final String shippingTerms;
private final Long validityDays;
private final ProgressTracker progressTracker = new ProgressTracker(
GENERATING_TRANSACTION,
VERIFYING_TRANSACTION,
SIGNING_TRANSACTION,
GATHERING_SIGNATURES,
FINALISING_TRANSACTION
);
public IssueLetterOfCreditFlow(Party beneficiary,
Party issuingBank,
String lcNumber,
Double amount,
String currency,
String goodsDescription,
String shippingTerms,
Long validityDays) {
this.beneficiary = beneficiary;
this.issuingBank = issuingBank;
this.lcNumber = lcNumber;
this.amount = amount;
this.currency = currency;
this.goodsDescription = goodsDescription;
this.shippingTerms = shippingTerms;
this.validityDays = validityDays;
}
@Override
public ProgressTracker getProgressTracker() {
return progressTracker;
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
// Get current node identity
final Party applicant = getOurIdentity();
progressTracker.setCurrentStep(GENERATING_TRANSACTION);
// Create the output state
final UniqueIdentifier linearId = new UniqueIdentifier();
final Instant issueDate = Instant.now();
final Instant expiryDate = issueDate.plus(validityDays, ChronoUnit.DAYS);
final LetterOfCreditState outputState = new LetterOfCreditState(
linearId,
applicant,
beneficiary,
issuingBank,
null, // No advising bank initially
lcNumber,
amount,
currency,
goodsDescription,
shippingTerms,
LetterOfCreditState.LCStatus.ISSUED,
issueDate,
expiryDate
);
// Build transaction
final TransactionBuilder txBuilder = new TransactionBuilder();
// Get notary
final Party notary = getServiceHub().getNetworkMapCache()
.getNotaryIdentities().get(0);
txBuilder.setNotary(notary);
// Add output state and command
final Command<LetterOfCreditContract.Commands.Issue> issueCommand = 
new Command<>(new LetterOfCreditContract.Commands.Issue(),
Arrays.asList(applicant.getOwningKey(), issuingBank.getOwningKey()));
txBuilder.addOutputState(outputState, LetterOfCreditContract.ID);
txBuilder.addCommand(issueCommand);
progressTracker.setCurrentStep(VERIFYING_TRANSACTION);
// Verify transaction
txBuilder.verify(getServiceHub());
progressTracker.setCurrentStep(SIGNING_TRANSACTION);
// Sign transaction
final SignedTransaction partiallySignedTx = getServiceHub().signInitialTransaction(txBuilder);
progressTracker.setCurrentStep(GATHERING_SIGNATURES);
// Collect counterparty signatures
final FlowSession issuingBankSession = initiateFlow(issuingBank);
final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
partiallySignedTx, Collections.singletonList(issuingBankSession)));
progressTracker.setCurrentStep(FINALISING_TRANSACTION);
// Finalize transaction
return subFlow(new FinalityFlow(fullySignedTx, Arrays.asList(issuingBankSession)));
}
}

Responder Flow for LC Issuance:

@InitiatedBy(IssueLetterOfCreditFlow.class)
public class IssueLetterOfCreditResponder extends FlowLogic<SignedTransaction> {
private final FlowSession otherPartySession;
public IssueLetterOfCreditResponder(FlowSession otherPartySession) {
this.otherPartySession = otherPartySession;
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
class SignTxFlow extends SignTransactionFlow {
private SignTxFlow(FlowSession otherPartySession) {
super(otherPartySession);
}
@Override
protected void checkTransaction(SignedTransaction stx) {
// Add any custom verification logic for the issuing bank
// For example, check if the bank's policies are satisfied
}
}
final SignedTransaction signedTx = subFlow(new SignTxFlow(otherPartySession));
return subFlow(new ReceiveFinalityFlow(otherPartySession, signedTx.getId()));
}
}

Advise Letter of Credit Flow:

@InitiatingFlow
@StartableByRPC
public class AdviseLetterOfCreditFlow extends FlowLogic<SignedTransaction> {
private final UniqueIdentifier linearId;
private final Party advisingBank;
private final ProgressTracker progressTracker = new ProgressTracker(
RETRIEVING_STATE,
BUILDING_TRANSACTION,
VERIFYING_TRANSACTION,
SIGNING_TRANSACTION,
GATHERING_SIGNATURES,
FINALISING_TRANSACTION
);
public AdviseLetterOfCreditFlow(UniqueIdentifier linearId, Party advisingBank) {
this.linearId = linearId;
this.advisingBank = advisingBank;
}
@Suspendable
@Override
public SignedTransaction call() throws FlowException {
progressTracker.setCurrentStep(RETRIEVING_STATE);
// Retrieve the LC state from vault
final QueryCriteria.LinearStateQueryCriteria criteria = 
new QueryCriteria.LinearStateQueryCriteria(
null, 
Collections.singletonList(linearId.getId()),
Vault.StateStatus.UNCONSUMED,
null);
final StateAndRef<LetterOfCreditState> inputStateAndRef = 
getServiceHub().getVaultService()
.queryBy(LetterOfCreditState.class, criteria)
.getStates().get(0);
final LetterOfCreditState inputState = inputStateAndRef.getState().getData();
// Verify we are the beneficiary
if (!inputState.getBeneficiary().equals(getOurIdentity())) {
throw new FlowException("Only beneficiary can advise an LC");
}
progressTracker.setCurrentStep(BUILDING_TRANSACTION);
// Create advised state
final LetterOfCreditState outputState = inputState
.withAdvisingBank(advisingBank)
.withStatus(LetterOfCreditState.LCStatus.ADVISED);
// Build transaction
final TransactionBuilder txBuilder = new TransactionBuilder();
final Party notary = inputStateAndRef.getState().getNotary();
txBuilder.setNotary(notary);
// Add input and output states
txBuilder.addInputState(inputStateAndRef);
txBuilder.addOutputState(outputState, LetterOfCreditContract.ID);
// Add command
final Command<LetterOfCreditContract.Commands.Advise> adviseCommand = 
new Command<>(new LetterOfCreditContract.Commands.Advise(),
Collections.singletonList(advisingBank.getOwningKey()));
txBuilder.addCommand(adviseCommand);
progressTracker.setCurrentStep(VERIFYING_TRANSACTION);
txBuilder.verify(getServiceHub());
progressTracker.setCurrentStep(SIGNING_TRANSACTION);
final SignedTransaction partiallySignedTx = getServiceHub().signInitialTransaction(txBuilder);
progressTracker.setCurrentStep(GATHERING_SIGNATURES);
final FlowSession advisingBankSession = initiateFlow(advisingBank);
final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
partiallySignedTx, Collections.singletonList(advisingBankSession)));
progressTracker.setCurrentStep(FINALISING_TRANSACTION);
return subFlow(new FinalityFlow(fullySignedTx, Arrays.asList(advisingBankSession)));
}
}

5. API Development

Expose CorDapp functionality via REST API:

package com.tradefinance.api;
import com.tradefinance.flows.AdviseLetterOfCreditFlow;
import com.tradefinance.flows.IssueLetterOfCreditFlow;
import com.tradefinance.states.LetterOfCreditState;
import net.corda.core.contracts.StateAndRef;
import net.corda.core.identity.CordaX500Name;
import net.corda.core.identity.Party;
import net.corda.core.messaging.CordaRPCOps;
import net.corda.core.node.NodeInfo;
import net.corda.core.node.services.Vault;
import net.corda.core.node.services.vault.QueryCriteria;
import net.corda.core.transactions.SignedTransaction;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.stream.Collectors;
@Path("letterofcredit")
public class LetterOfCreditApi {
private final CordaRPCOps rpcOps;
private final Party myIdentity;
public LetterOfCreditApi(CordaRPCOps rpcOps) {
this.rpcOps = rpcOps;
this.myIdentity = rpcOps.nodeInfo().getLegalIdentities().get(0);
}
@GET
@Path("mylcs")
@Produces(MediaType.APPLICATION_JSON)
public List<LetterOfCreditState> getMyLetterOfCredits() {
QueryCriteria.VaultQueryCriteria criteria = 
new QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED);
List<StateAndRef<LetterOfCreditState>> lcStates = 
rpcOps.vaultQueryByCriteria(criteria, LetterOfCreditState.class).getStates();
return lcStates.stream()
.map(stateAndRef -> stateAndRef.getState().getData())
.collect(Collectors.toList());
}
@GET
@Path("lcs-as-applicant")
@Produces(MediaType.APPLICATION_JSON)
public List<LetterOfCreditState> getLCsAsApplicant() {
return getMyLetterOfCredits().stream()
.filter(lc -> lc.getApplicant().equals(myIdentity))
.collect(Collectors.toList());
}
@GET
@Path("lcs-as-beneficiary")
@Produces(MediaType.APPLICATION_JSON)
public List<LetterOfCreditState> getLCsAsBeneficiary() {
return getMyLetterOfCredits().stream()
.filter(lc -> lc.getBeneficiary().equals(myIdentity))
.collect(Collectors.toList());
}
@POST
@Path("issue")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response issueLetterOfCredit(IssueLCRequest request) {
try {
// Find counterparties from network map
Party beneficiary = resolveParty(request.getBeneficiaryName());
Party issuingBank = resolveParty(request.getIssuingBankName());
// Start flow
SignedTransaction result = rpcOps.startTrackedFlowDynamic(
IssueLetterOfCreditFlow.class,
beneficiary,
issuingBank,
request.getLcNumber(),
request.getAmount(),
request.getCurrency(),
request.getGoodsDescription(),
request.getShippingTerms(),
request.getValidityDays()
).getReturnValue().get();
return Response.status(Response.Status.CREATED)
.entity(new FlowResult("LC issued successfully", result.getId().toString()))
.build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@POST
@Path("advise")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response adviseLetterOfCredit(AdviseLCRequest request) {
try {
Party advisingBank = resolveParty(request.getAdvisingBankName());
SignedTransaction result = rpcOps.startTrackedFlowDynamic(
AdviseLetterOfCreditFlow.class,
request.getLinearId(),
advisingBank
).getReturnValue().get();
return Response.ok(new FlowResult("LC advised successfully", result.getId().toString()))
.build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@GET
@Path("network-parties")
@Produces(MediaType.APPLICATION_JSON)
public List<PartyInfo> getNetworkParties() {
return rpcOps.networkMapSnapshot().stream()
.map(NodeInfo::getLegalIdentities)
.flatMap(List::stream)
.map(party -> new PartyInfo(
party.getName().getOrganisation(),
party.getName().getLocality(),
party.getName().getCountry()
))
.distinct()
.collect(Collectors.toList());
}
private Party resolveParty(String organisation) {
return rpcOps.networkMapSnapshot().stream()
.flatMap(node -> node.getLegalIdentities().stream())
.filter(party -> party.getName().getOrganisation().equals(organisation))
.findFirst()
.orElseThrow(() -> new WebApplicationException("Party not found: " + organisation, 404));
}
// DTO classes
public static class IssueLCRequest {
private String beneficiaryName;
private String issuingBankName;
private String lcNumber;
private Double amount;
private String currency;
private String goodsDescription;
private String shippingTerms;
private Long validityDays;
// getters and setters
public String getBeneficiaryName() { return beneficiaryName; }
public void setBeneficiaryName(String beneficiaryName) { this.beneficiaryName = beneficiaryName; }
public String getIssuingBankName() { return issuingBankName; }
public void setIssuingBankName(String issuingBankName) { this.issuingBankName = issuingBankName; }
public String getLcNumber() { return lcNumber; }
public void setLcNumber(String lcNumber) { this.lcNumber = lcNumber; }
public Double getAmount() { return amount; }
public void setAmount(Double amount) { this.amount = amount; }
public String getCurrency() { return currency; }
public void setCurrency(String currency) { this.currency = currency; }
public String getGoodsDescription() { return goodsDescription; }
public void setGoodsDescription(String goodsDescription) { this.goodsDescription = goodsDescription; }
public String getShippingTerms() { return shippingTerms; }
public void setShippingTerms(String shippingTerms) { this.shippingTerms = shippingTerms; }
public Long getValidityDays() { return validityDays; }
public void setValidityDays(Long validityDays) { this.validityDays = validityDays; }
}
public static class AdviseLCRequest {
private net.corda.core.contracts.UniqueIdentifier linearId;
private String advisingBankName;
// getters and setters
public net.corda.core.contracts.UniqueIdentifier getLinearId() { return linearId; }
public void setLinearId(net.corda.core.contracts.UniqueIdentifier linearId) { this.linearId = linearId; }
public String getAdvisingBankName() { return advisingBankName; }
public void setAdvisingBankName(String advisingBankName) { this.advisingBankName = advisingBankName; }
}
public static class FlowResult {
private final String message;
private final String transactionId;
public FlowResult(String message, String transactionId) {
this.message = message;
this.transactionId = transactionId;
}
public String getMessage() { return message; }
public String getTransactionId() { return transactionId; }
}
public static class ErrorResponse {
private final String error;
public ErrorResponse(String error) {
this.error = error;
}
public String getError() { return error; }
}
public static class PartyInfo {
private final String organisation;
private final String locality;
private final String country;
public PartyInfo(String organisation, String locality, String country) {
this.organisation = organisation;
this.locality = locality;
this.country = country;
}
public String getOrganisation() { return organisation; }
public String getLocality() { return locality; }
public String getCountry() { return country; }
}
}

6. Testing CorDapps

Comprehensive testing for states, contracts, and flows:

package com.tradefinance.test;
import com.tradefinance.contracts.LetterOfCreditContract;
import com.tradefinance.flows.IssueLetterOfCreditFlow;
import com.tradefinance.states.LetterOfCreditState;
import net.corda.core.contracts.Command;
import net.corda.core.contracts.TransactionState;
import net.corda.core.identity.CordaX500Name;
import net.corda.core.identity.Party;
import net.corda.core.transactions.TransactionBuilder;
import net.corda.testing.core.TestIdentity;
import net.corda.testing.node.MockServices;
import net.corda.testing.node.StartedMockNode;
import net.corda.testing.node.TestNetwork;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
import static net.corda.testing.node.NodeTestUtils.ledger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class LetterOfCreditTests {
private TestNetwork network;
private StartedMockNode applicantNode;
private StartedMockNode beneficiaryNode;
private StartedMockNode issuingBankNode;
private StartedMockNode advisingBankNode;
private TestIdentity applicant;
private TestIdentity beneficiary;
private TestIdentity issuingBank;
private TestIdentity advisingBank;
private final MockServices ledgerServices = new MockServices(
Arrays.asList("com.tradefinance")
);
@Before
public void setup() {
network = new TestNetwork();
applicantNode = network.createNode();
beneficiaryNode = network.createNode();
issuingBankNode = network.createNode();
advisingBankNode = network.createNode();
applicant = new TestIdentity(new CordaX500Name("Applicant", "London", "GB"));
beneficiary = new TestIdentity(new CordaX500Name("Beneficiary", "New York", "US"));
issuingBank = new TestIdentity(new CordaX500Name("IssuingBank", "London", "GB"));
advisingBank = new TestIdentity(new CordaX500Name("AdvisingBank", "New York", "US"));
network.runNetwork();
}
@After
public void tearDown() {
network.stopNodes();
}
@Test
public void testLetterOfCreditStateCreation() {
LetterOfCreditState.LCStatus status = LetterOfCreditState.LCStatus.ISSUED;
Instant issueDate = Instant.now();
Instant expiryDate = issueDate.plus(30, ChronoUnit.DAYS);
LetterOfCreditState state = new LetterOfCreditState(
new net.corda.core.contracts.UniqueIdentifier(),
applicant.getParty(),
beneficiary.getParty(),
issuingBank.getParty(),
null,
"LC123456",
100000.0,
"USD",
"Electronics Components",
"CIF",
status,
issueDate,
expiryDate
);
assertEquals(applicant.getParty(), state.getApplicant());
assertEquals(beneficiary.getParty(), state.getBeneficiary());
assertEquals("LC123456", state.getLcNumber());
assertEquals(LetterOfCreditState.LCStatus.ISSUED, state.getStatus());
assertEquals(3, state.getParticipants().size());
}
@Test
public void testIssueTransaction() {
ledger(ledgerServices, l -> {
l.transaction(tx -> {
Instant issueDate = Instant.now();
Instant expiryDate = issueDate.plus(30, ChronoUnit.DAYS);
LetterOfCreditState outputState = new LetterOfCreditState(
new net.corda.core.contracts.UniqueIdentifier(),
applicant.getParty(),
beneficiary.getParty(),
issuingBank.getParty(),
null,
"LC123456",
100000.0,
"USD",
"Electronics Components",
"CIF",
LetterOfCreditState.LCStatus.ISSUED,
issueDate,
expiryDate
);
Command<LetterOfCreditContract.Commands.Issue> issueCommand = 
new Command<>(new LetterOfCreditContract.Commands.Issue(),
Arrays.asList(applicant.getParty().getOwningKey(), 
issuingBank.getParty().getOwningKey()));
tx.output(LetterOfCreditContract.ID, outputState);
tx.command(issueCommand);
tx.verifies();
return null;
});
return null;
});
}
@Test
public void testIssueFlow() throws Exception {
IssueLetterOfCreditFlow flow = new IssueLetterOfCreditFlow(
beneficiary.getParty(),
issuingBank.getParty(),
"LC123456",
100000.0,
"USD",
"Electronics Components",
"CIF",
30L
);
applicantNode.startFlow(flow);
network.runNetwork();
// Verify state is recorded in vault
applicantNode.transaction(() -> {
assertEquals(1, applicantNode.getServices().getVaultService()
.queryBy(LetterOfCreditState.class).getStates().size());
return null;
});
}
@Test
public void testInvalidIssueTransaction() {
ledger(ledgerServices, l -> {
l.transaction(tx -> {
Instant issueDate = Instant.now();
Instant expiryDate = issueDate.minus(1, ChronoUnit.DAYS); // Invalid: expiry before issue
LetterOfCreditState outputState = new LetterOfCreditState(
new net.corda.core.contracts.UniqueIdentifier(),
applicant.getParty(),
beneficiary.getParty(),
issuingBank.getParty(),
null,
"LC123456",
-1000.0, // Invalid: negative amount
"USD",
"Electronics Components",
"CIF",
LetterOfCreditState.LCStatus.ISSUED,
issueDate,
expiryDate
);
Command<LetterOfCreditContract.Commands.Issue> issueCommand = 
new Command<>(new LetterOfCreditContract.Commands.Issue(),
Arrays.asList(applicant.getParty().getOwningKey(), 
issuingBank.getParty().getOwningKey()));
tx.output(LetterOfCreditContract.ID, outputState);
tx.command(issueCommand);
tx.failsWith("LC amount must be positive");
return null;
});
return null;
});
}
}

7. Deployment and Configuration

Node Configuration (node.conf):

# Node configuration
myLegalName="O=Applicant,L=London,C=GB"
p2pAddress="localhost:10005"
rpcSettings {
address="localhost:10006"
adminAddress="localhost:10046"
}
rpcUsers=[
{
username=user1
password=letmein
permissions=[
ALL
]
}
]
# Cordapp configuration
cordappSignerKeyFingerprintBlacklist = []
cordappsDirectory = "cordapps"
devMode = true
# Network services
networkServices {
doormanURL="https://doorman.corda.network"
networkMapURL="https://netmap.corda.network"
}
# Security
keyStorePassword = "cordacadevpass"
trustStorePassword = "trustpass"
# Database
dataSourceProperties = {
dataSourceClassName = "org.h2.jdbcx.JdbcDataSource"
"dataSource.url" = "jdbc:h2:file:"${baseDirectory}"/persistence"
"dataSource.user" = sa
"dataSource.password" = ""
}
database = {
transactionIsolationLevel = READ_COMMITTED
}

Docker Deployment:

FROM corda/corda-zulu-java1.8-4.9:latest
USER root
# Create Corda user
RUN adduser --disabled-password --gecos '' corda
# Create directories
RUN mkdir -p /opt/corda/cordapps
RUN mkdir -p /opt/corda/logs
RUN mkdir -p /opt/corda/certificates
# Copy CorDapp JAR
COPY build/libs/trade-finance-cordapp-1.0.jar /opt/corda/cordapps/
# Copy node configuration
COPY node.conf /opt/corda/node.conf
# Set permissions
RUN chown -R corda:corda /opt/corda
USER corda
WORKDIR /opt/corda
EXPOSE 10005 10006 10007 10008 10009 10010 10011 10012 10013 10014 10015 10016
CMD ["bash", "-c", "java -jar /opt/corda/corda.jar"]

Best Practices for CorDapp Development

1. State Design:

  • Keep states focused and minimal
  • Use LinearState for evolving assets
  • Include all relevant participants
  • Consider privacy requirements

2. Contract Verification:

  • Verify all possible command types
  • Check signer requirements thoroughly
  • Validate business rules explicitly
  • Use descriptive error messages

3. Flow Development:

  • Use progress tracking for long-running flows
  • Implement proper error handling
  • Consider flow versioning
  • Test with multiple parties

4. Security Considerations:

public class SecurityBestPractices {
// Always verify transaction signatures
private void verifySigners(Set<PublicKey> requiredSigners, Set<PublicKey> actualSigners) {
if (!actualSigners.containsAll(requiredSigners)) {
throw new IllegalArgumentException("Missing required signatures");
}
}
// Validate time windows
private void validateTimeWindow(Instant timestamp, Instant expiry) {
if (timestamp.isAfter(expiry)) {
throw new IllegalArgumentException("Transaction expired");
}
}
}

Common CorDapp Patterns

1. State Evolution:

public class StateEvolution {
public LetterOfCreditState evolveState(LetterOfCreditState current, 
LetterOfCreditState.LCStatus newStatus) {
return current.withStatus(newStatus);
}
}

2. Querying the Vault:

public class VaultQueries {
public List<StateAndRef<LetterOfCreditState>> queryLCsByStatus(
ServiceHub serviceHub, LetterOfCreditState.LCStatus status) {
QueryCriteria.VaultCustomQueryCriteria criteria = 
new QueryCriteria.VaultCustomQueryCriteria(
Builder.equal(getStatusField("status"), status.name()));
return serviceHub.getVaultService()
.queryBy(LetterOfCreditState.class, criteria)
.getStates();
}
}

Conclusion

Corda CorDapp development in Java enables building enterprise-grade blockchain applications with strong privacy guarantees. Key takeaways:

  1. States: Represent shared facts with clear ownership
  2. Contracts: Enforce business rules with legal precision
  3. Flows: Coordinate complex multi-party processes
  4. Transactions: Ensure atomic updates across parties
  5. Privacy: Share data only with relevant participants

Implementation Checklist:

  • ✅ Design focused states with clear participants
  • ✅ Implement comprehensive contract verification
  • ✅ Develop robust flows with error handling
  • ✅ Create REST APIs for external integration
  • ✅ Write comprehensive tests for all components
  • ✅ Configure nodes for target environment
  • ✅ Plan for deployment and operations

Corda is particularly well-suited for:

  • Financial services applications
  • Supply chain and trade finance
  • Healthcare data sharing
  • Legal and regulatory compliance
  • Any scenario requiring privacy and legal enforceability

By following these patterns and best practices, you can build robust, secure, and scalable distributed applications on the Corda platform.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper