google-site-verification: google61fe8ba583a51912.html
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