Upgrading CorDapps to newer Platform Versions

These notes provide instructions for upgrading your CorDapps from previous versions. Corda provides backwards compatibility for public, non-experimental APIs that have been committed to. A list can be found in the API stability guarantees page.

This means that you can upgrade your node across versions without recompiling or adjusting your CorDapps. You just have to upgrade your node and restart.

However, there are usually new features and other opt-in changes that may improve the security, performance or usability of your application that are worth considering for any actively maintained software. This guide shows you how to upgrade your app to benefit from the new features in the latest release.

Corda releasePlatform version
4.911
4.810
4.79
4.68
4.57
4.46
4.35
4.24
4.14
4.04
3.33

No manual upgrade steps are required.

No manual upgrade steps are required.

No manual upgrade steps are required.

The operational improvements around database schema harmonisation that we have made in Corda 4.6 require a number of manual steps when upgrading to Corda 4.6 from a previous version.

The required steps for each upgrade path are described below.

  1. Remove any entries of transactionIsolationLevel, initialiseSchema, or initialiseAppSchema from the database section of your node configuration file.
  2. Update any missing core schema changes by running the node in run-migration-scripts mode: java -jar corda.jar run-migration-scripts --core-schemas.
  3. Add Liquibase resources to CorDapps. In Corda 4.6, CorDapps that introduce custom schema need Liquibase migration scripts allowing them to create the schema upfront. For existing CorDapps that do not have migration scripts in their resources, they can be added as an external migration .jar file, as documented in the archived 4.6 Corda Enterprise documentation.
  4. Update the changelog for existing schemas. After upgrading the Corda .jar file and adding Liquibase scripts to the CorDapp(s), any custom schemas from the apps are present in the database, but the changelog entries in the Liquibase changelog table are missing (as they have been created by Liquibase). This will cause issues when starting the node, and also when running run-migration-scripts as tables that already exist cannot be recreated. By running the new sub-command sync-app-schemas, changelog entries are created for all existing mapped schemas from CorDapps: java -jar corda.jar sync-app-schemas.

Corda 4.6 drops the support for retro-fitting the database changelog when migrating from Corda versions older than 4.0. Thus it is required to migrate to a previous 4.x version before migrating to Corda 4.6 - for example, 3.3 to 4.5, and then 4.5 to 4.6.

To successfully build a CorDapp against Platform Version 8 and Corda 4.6, you need to use version 5.0.12 of the Corda Gradle Plugins:

ext.corda_gradle_plugins_version = '5.0.12'

No manual upgrade steps are required.

This section provides instructions for upgrading your CorDapps from previous versions to take advantage of features and enhancements introduced in platform version 5.

The following code, which compiled in Platform Version 4, will not compile in Platform Version 5:

data class Obligation(val amount: Amount<Currency>, val lender: AbstractParty, val borrower: AbstractParty)

val (lenderId, borrowerId) = if (anonymous) {
    val anonymousIdentitiesResult = subFlow(SwapIdentitiesFlow(lenderSession))
    Pair(anonymousIdentitiesResult[lenderSession.counterparty]!!, anonymousIdentitiesResult[ourIdentity]!!)
} else {
    Pair(lender, ourIdentity)
}

val obligation = Obligation(100.dollars, lenderId, borrowerId)

Compiling this code against Platform Version 5 will result in the following error:

Type mismatch: inferred type is Any but AbstractParty was expected

The issue here is that a new Destination interface introduced in Platform Version 5 can cause type inference failures when a variable is used as an AbstractParty but has an actual value that is one of Party or AnonymousParty. These subclasses implement Destination, while the superclass does not. Kotlin must pick a type for the variable, and so chooses the most specific ancestor of both AbstractParty and Destination. This is Any, which is not a valid type for use as an AbstractParty later. For more information on Destination, see the Changelog for Platform Version 5, or the KDocs for the interface here.

Note that this is a Kotlin-specific issue. Java can instead choose ? extends AbstractParty & Destination here, which can later be used as AbstractParty.

To fix this, an explicit type hint must be provided to the compiler:

data class Obligation(val amount: Amount<Currency>, val lender: AbstractParty, val borrower: AbstractParty)

val (lenderId, borrowerId) = if (anonymous) {
    val anonymousIdentitiesResult = subFlow(SwapIdentitiesFlow(lenderSession))
    Pair(anonymousIdentitiesResult[lenderSession.counterparty]!!, anonymousIdentitiesResult[ourIdentity]!!)
} else {
    // This Pair now provides a type hint to the compiler
    Pair<AbstractParty, AbstractParty>(lender, ourIdentity)
}

val obligation = Obligation(100.dollars, lenderId, borrowerId)

This stops type inference from occurring and forces the variable to be of type AbstractParty.

Platform Version 5 requires Gradle 5.4 to build. If you use the Gradle wrapper, you can upgrade by running:

./gradlew wrapper --gradle-version 5.4.1

Otherwise, upgrade your installed copy in the usual manner for your operating system.

Additionally, you’ll need to add https://repo.gradle.org/gradle/libs-releases as a repository to your project, in order to pick up the gradle-api-tooling dependency. You can do this by adding the following to the repositories in your Gradle file:

maven { url 'https://repo.gradle.org/gradle/libs-releases' }

This section provides instructions for upgrading your CorDapps from previous versions to platform version 4.

Although the RPC API is backwards compatible with Corda 3, the RPC wire protocol isn’t. Therefore RPC clients like web servers need to be updated in lockstep with the node to use the new version of the RPC library. Corda 4 delivers RPC wire stability and therefore in future you will be able to update the node and apps without updating RPC clients.

Alter the versions you depend on in your Gradle file like so:

ext.corda_release_version = '4.4'
ext.corda_gradle_plugins_version = '5.0.6'
ext.kotlin_version = '1.2.71'
ext.quasar_version = '0.7.12_r3'

You should also ensure you’re using Gradle 4.10 (but not 5). If you use the Gradle wrapper, run:

./gradlew wrapper --gradle-version 4.10.3

Otherwise just upgrade your installed copy in the usual manner for your operating system.

There are several adjustments that are beneficial to make to your Gradle build file, beyond simply incrementing the versions as described in step 1.

Provide app metadata. This is used by the Corda Gradle build plugin to populate your app JAR with useful information. It should look like this:

cordapp {
    targetPlatformVersion 4
    minimumPlatformVersion 4
    contract {
        name "MegaApp Contracts"
        vendor "MegaCorp"
        licence "A liberal, open source licence"
        versionId 1
    }
    workflow {
        name "MegaApp flows"
        vendor "MegaCorp"
        licence "A really expensive proprietary licence"
        versionId 1
    }
}

Name, vendor and licence can be set to any string you like, they don’t have to be Corda identities.

Target versioning is a new concept introduced in Corda 4. Learn more by reading Versioning. Setting a target version of 4 opts in to changes that might not be 100% backwards compatible, such as API semantics changes or disabling workarounds for bugs that may be in your apps, so by doing this you are promising that you have thoroughly tested your app on the new version. Using a high target version is a good idea because some features and improvements are only available to apps that opt in.

The minimum platform version is the platform version of the node that you require, so if you start using new APIs and features in Corda 4, you should set this to 4. Unfortunately Corda 3 and below do not know about this metadata and don’t check it, so your app will still be loaded in such nodes and may exhibit undefined behaviour at runtime. However it’s good to get in the habit of setting this properly for future releases.

The new versionId number is a version code for your app, and is unrelated to Corda’s own versions. It is currently used for informative purposes only.

Split your app into contract and workflow JARs. The duplication between contract and workflow blocks exists because you should split your app into two separate JARs/modules, one that contains on-ledger validation code like states and contracts, and one for the rest (called by convention the “workflows” module although it can contain a lot more than just flows: services would also go here, for instance). For simplicity, here we use one JAR for both, but this is in general an anti-pattern and can result in your flow logic code being sent over the network to arbitrary third party peers, even though they don’t need it.

In future, the version ID attached to the workflow JAR will also be used to help implement smoother upgrade and migration features. You may directly reference the gradle version number of your app when setting the CorDapp specific versionId identifiers if this follows the convention of always being a whole number starting from 1.

If you use the finance demo app, you should adjust your dependencies so you depend on the finance-contracts and finance-workflows artifacts from your own contract and workflow JAR respectively.

CorDapps can no longer access custom configuration items in the node.conf file. Any custom CorDapp configuration should be added to a CorDapp configuration file. The Node’s configuration will not be accessible. CorDapp configuration files should be placed in the config subdirectory of the Node’s cordapps folder. The name of the file should match the name of the JAR of the CorDapp (eg; if your CorDapp is called hello-0.1.jar the configuration file needed would be cordapps/config/hello-0.1.conf).

If you are using the extraConfig of a node in the deployNodes Gradle task to populate custom configuration for testing, you will need to make the following change so that:

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    node {
        name "O=Bank A,L=London,C=GB"c
        ...
        extraConfig = [ 'some.extra.config' : '12345' ]
    }
}

Would become:

task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
    node {
        name "O=Bank A,L=London,C=GB"c
        ...
        projectCordapp {
            config "some.extra.config=12345"
        }
    }
}

See CorDapp configuration files for more information.

The previous FinalityFlow API is insecure. It doesn’t have a receive flow, so requires counterparty nodes to accept any and all signed transactions that are sent to it, without checks. It is highly recommended that existing CorDapps migrate away to the new API, as otherwise things like business network membership checks won’t be reliably enforced.

The flows that make use of FinalityFlow in a CorDapp can be classified in the following 2 basic categories:

  • non-initiating flows: these are flows that finalise a transaction without the involvement of a counterpart flow at all.
  • initiating flows: these are flows that initiate a counterpart (responder) flow.

There is a main difference between these 2 different categories, which is relevant to how the CorDapp can be upgraded. The second category of flows can be upgraded to use the new FinalityFlow in a backwards compatible way, which means the upgraded CorDapp can be deployed at the various nodes using a rolling deployment. On the other hand, the first category of flows cannot be upgraded to the new FinalityFlow in a backwards compatible way, so the changes to these flows need to be deployed simultaneously at all the nodes, using a lockstep deployment.

The upgrade is a three step process:

As an example, let’s take a very simple flow that finalises a transaction without the involvement of a counterpart flow:

class SimpleFlowUsingOldApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
        val stx = dummyTransactionWithParticipant(counterparty)
        return subFlow(FinalityFlow(stx))
    }
}
public static class SimpleFlowUsingOldApi extends FlowLogic<SignedTransaction> {
    private final Party counterparty;

    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
        return subFlow(new FinalityFlow(stx));
    }

To use the new API, this flow needs to be annotated with InitiatingFlow and a FlowSession to the participant(s) of the transaction must be passed to FinalityFlow :

// Notice how the flow *must* now be an initiating flow even when it wasn't before.
@InitiatingFlow
class SimpleFlowUsingNewApi(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
        val stx = dummyTransactionWithParticipant(counterparty)
        // For each non-local participant in the transaction we must initiate a flow session with them.
        val session = initiateFlow(counterparty)
        return subFlow(FinalityFlow(stx, session))
    }
}
// Notice how the flow *must* now be an initiating flow even when it wasn't before.
@InitiatingFlow
public static class SimpleFlowUsingNewApi extends FlowLogic<SignedTransaction> {
    private final Party counterparty;

    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        SignedTransaction stx = dummyTransactionWithParticipant(counterparty);
        // For each non-local participant in the transaction we must initiate a flow session with them.
        FlowSession session = initiateFlow(counterparty);
        return subFlow(new FinalityFlow(stx, session));
    }

If there are more than one transaction participants then a session to each one must be initiated, excluding the local party and the notary.

A responder flow has to be introduced, which will automatically run on the other participants’ nodes, which will call ReceiveFinalityFlow to record the finalised transaction:

// All participants will run this flow to receive and record the finalised transaction into their vault.
@InitiatedBy(SimpleFlowUsingNewApi::class)
class SimpleNewResponderFlow(private val otherSide: FlowSession) : FlowLogic<Unit>() {
    @Suspendable
    override fun call() {
        subFlow(ReceiveFinalityFlow(otherSide))
    }
}
// All participants will run this flow to receive and record the finalised transaction into their vault.
@InitiatedBy(SimpleFlowUsingNewApi.class)
public static class SimpleNewResponderFlow extends FlowLogic<Void> {
    private final FlowSession otherSide;

    @Suspendable
    @Override
    public Void call() throws FlowException {
        subFlow(new ReceiveFinalityFlow(otherSide));
        return null;
    }

For flows which are already initiating counterpart flows then it’s a matter of using the existing flow session. Note however, the new FinalityFlow is inlined and so the sequence of sends and receives between the two flows will change and will be incompatible with your current flows. You can use the flow version API to write your flows in a backwards compatible manner.

Here’s what an upgraded initiating flow may look like:

// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
// to allow for backwards compatibility with nodes running the old CorDapp.
@InitiatingFlow(version = 2)
class ExistingInitiatingFlow(private val counterparty: Party) : FlowLogic<SignedTransaction>() {
    @Suspendable
    override fun call(): SignedTransaction {
        val partiallySignedTx = dummyTransactionWithParticipant(counterparty)
        val session = initiateFlow(counterparty)
        val fullySignedTx = subFlow(CollectSignaturesFlow(partiallySignedTx, listOf(session)))
        // Determine which version of the flow that other side is using.
        return if (session.getCounterpartyFlowInfo().flowVersion == 1) {
            // Use the old API if the other side is using the previous version of the flow.
            subFlow(FinalityFlow(fullySignedTx))
        } else {
            // Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
            subFlow(FinalityFlow(fullySignedTx, session))
        }
    }
}
// Assuming the previous version of the flow was 1 (the default if none is specified), we increment the version number to 2
// to allow for backwards compatibility with nodes running the old CorDapp.
@InitiatingFlow(version = 2)
public static class ExistingInitiatingFlow extends FlowLogic<SignedTransaction> {
    private final Party counterparty;

    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        SignedTransaction partiallySignedTx = dummyTransactionWithParticipant(counterparty);
        FlowSession session = initiateFlow(counterparty);
        SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(partiallySignedTx, singletonList(session)));
        // Determine which version of the flow that other side is using.
        if (session.getCounterpartyFlowInfo().getFlowVersion() == 1) {
            // Use the old API if the other side is using the previous version of the flow.
            return subFlow(new FinalityFlow(fullySignedTx));
        } else {
            // Otherwise they're at least on version 2 and so we can send the finalised transaction on the existing session.
            return subFlow(new FinalityFlow(fullySignedTx, session));
        }
    }

For the responder flow, insert a call to ReceiveFinalityFlow at the location where it’s expecting to receive the finalised transaction. If the initiator is written in a backwards compatible way then so must the responder.

// First we have to run the SignTransactionFlow, which will return a SignedTransaction.
val txWeJustSigned = subFlow(object : SignTransactionFlow(otherSide) {
    @Suspendable
    override fun checkTransaction(stx: SignedTransaction) {
        // Implement responder flow transaction checks here
    }
})

if (otherSide.getCounterpartyFlowInfo().flowVersion >= 2) {
    // The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
    // If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
    // that was just signed.
    subFlow(ReceiveFinalityFlow(otherSide, expectedTxId = txWeJustSigned.id))
} else {
    // Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
    // will automatically record the finalised transaction using the old insecure mechanism.
}
// First we have to run the SignTransactionFlow, which will return a SignedTransaction.
SignedTransaction txWeJustSigned = subFlow(new SignTransactionFlow(otherSide) {
    @Suspendable
    @Override
    protected void checkTransaction(@NotNull SignedTransaction stx) throws FlowException {
        // Implement responder flow transaction checks here
    }
});

if (otherSide.getCounterpartyFlowInfo().getFlowVersion() >= 2) {
    // The other side is not using the old CorDapp so call ReceiveFinalityFlow to record the finalised transaction.
    // If SignTransactionFlow is used then we can verify the tranaction we receive for recording is the same one
    // that was just signed by passing the transaction id to ReceiveFinalityFlow.
    subFlow(new ReceiveFinalityFlow(otherSide, txWeJustSigned.getId()));
} else {
    // Otherwise the other side is running the old CorDapp and so we don't need to do anything further. The node
    // will automatically record the finalised transaction using the old insecure mechanism.
}

You may already be using waitForLedgerCommit in your responder flow for the finalised transaction to appear in the local node’s vault. Now that it’s calling ReceiveFinalityFlow, which effectively does the same thing, this is no longer necessary. The call to waitForLedgerCommit should be removed.

The Confidential identities API is experimental in Corda 3 and remains so in Corda 4. In this release, the SwapIdentitiesFlow has been adjusted in the same way as FinalityFlow above, to close problems with confidential identities being injectable into a node outside of other flow context. Old code will still work, but R3 recommends to adjust your call sites so a session is passed into the SwapIdentitiesFlow.

MockNodeParameters and functions creating it no longer use a lambda expecting a NodeConfiguration object. Use a MockNetworkConfigOverrides object instead. This is an API change we regret, but unfortunately in Corda 3 we accidentally exposed large amounts of the node internal code through this one API entry point. We have now insulated the test API from node internals and reduced the exposure.

If you are constructing a MockServices for testing contracts, and your contract uses the Cash contract from the finance app, you now need to explicitly add net.corda.finance.contracts to the list of cordappPackages. This is a part of the work to disentangle the finance app (which is really a demo app) from the Corda internals. Example:

val ledgerServices = MockServices(
    listOf("net.corda.examples.obligation", "net.corda.testing.contracts"),
    initialIdentity = TestIdentity(CordaX500Name("TestIdentity", "", "GB")),
    identityService = makeTestIdentityService()
)
MockServices ledgerServices = new MockServices(
    Arrays.asList("net.corda.examples.obligation", "net.corda.testing.contracts"),
    new TestIdentity(new CordaX500Name("TestIdentity", "", "GB")),
    makeTestIdentityService()
);

becomes:

val ledgerServices = MockServices(
    listOf("net.corda.examples.obligation", "net.corda.testing.contracts", "net.corda.finance.contracts"),
    initialIdentity = TestIdentity(CordaX500Name("TestIdentity", "", "GB")),
    identityService = makeTestIdentityService()
)
MockServices ledgerServices = new MockServices(
    Arrays.asList("net.corda.examples.obligation", "net.corda.testing.contracts", "net.corda.finance.contracts"),
    new TestIdentity(new CordaX500Name("TestIdentity", "", "GB")),
    makeTestIdentityService()
);

You may need to use the new TestCordapp API when testing with the node driver or mock network, especially if you decide to stick with the pre-Corda 4 FinalityFlow API. The previous way of pulling in CorDapps into your tests (i.e. via using the cordappPackages parameter) does not honour CorDapp versioning. The new API TestCordapp.findCordapp() discovers the CorDapps that contain the provided packages scanning the classpath, so you have to ensure that the classpath the tests are running under contains either the CorDapp .jar or (if using Gradle) the relevant Gradle sub-project. In the first case, the versioning information in the CorDapp .jar file will be maintained. In the second case, the versioning information will be retrieved from the Gradle cordapp task. For example, if you are using MockNetwork for your tests, the following code:

val mockNetwork = MockNetwork(
    cordappPackages = listOf("net.corda.examples.obligation", "net.corda.finance.contracts"),
    notarySpecs = listOf(MockNetworkNotarySpec(notary))
)
MockNetwork mockNetwork = new MockNetwork(
    Arrays.asList("net.corda.examples.obligation", "net.corda.finance.contracts"),
    new MockNetworkParameters().withNotarySpecs(Arrays.asList(new MockNetworkNotarySpec(notary)))
);

would need to be transformed into:

val mockNetwork = MockNetwork(
    MockNetworkParameters(
        cordappsForAllNodes = listOf(
            TestCordapp.findCordapp("net.corda.examples.obligation.contracts"),
            TestCordapp.findCordapp("net.corda.examples.obligation.flows")
        ),
        notarySpecs = listOf(MockNetworkNotarySpec(notary))
    )
)
MockNetwork mockNetwork = new MockNetwork(
    new MockNetworkParameters(
        Arrays.asList(
            TestCordapp.findCordapp("net.corda.examples.obligation.contracts"),
            TestCordapp.findCordapp("net.corda.examples.obligation.flows")
        )
    ).withNotarySpecs(Arrays.asList(new MockNetworkNotarySpec(notary)))
);

Note that every package should exist in only one CorDapp, otherwise the discovery process won’t be able to determine which one to use and you will most probably see an exception telling you There is more than one CorDapp containing the package. For instance, if you have 2 CorDapps containing the packages net.corda.examples.obligation.contracts and net.corda.examples.obligation.flows, you will get this error if you specify the package net.corda.examples.obligation.

In versions of the platform prior to v4, it was the responsibility of contract and flow logic to ensure that TransactionState objects contained the correct class name of the expected contract class. If these checks were omitted, it would be possible for a malicious counterparty to construct a transaction containing e.g. a cash state governed by a commercial paper contract. The contract would see that there were no commercial paper states in a transaction and do nothing, i.e. accept.

In Corda 4 the platform takes over this responsibility from the app, if the app has a target version of 4 or higher. A state is expected to be governed by a contract that is either:

  • The outer class of the state class, if the state is an inner class of a contract. This is a common design pattern.
  • Annotated with @BelongsToContract which specifies the contract class explicitly.

Learn more by reading Contract/State Agreement. If an app targets Corda 3 or lower (i.e. does not specify a target version), states that point to contracts outside their package will trigger a log warning but validation will proceed.

Signature Constraints are a new data model feature introduced in Corda 4. They make it much easier to deploy application upgrades smoothly and in a decentralised manner. Signature constraints are the new default mode for CorDapps, and the act of upgrading your app to use the version 4 Gradle plugins will result in your app being automatically signed, and new states automatically using new signature constraints selected automatically based on these signing keys.

You can read more about signature constraints and what they do in API: Contract Constraints. The TransactionBuilder class will automatically use them if your application JAR is signed. R3 recommends all JARs are signed. To learn how to sign your JAR files, read Signing the CorDapp. In dev mode, all JARs are signed by developer certificates. If a JAR that was signed with developer certificates is deployed to a production node, the node will refuse to start. Therefore to deploy apps built for Corda 4 to production you will need to generate signing keys and integrate them with the build process.

Almost no apps will be affected by these changes, but they’re important to know about.

There are two improvements to how Java package protection is handled in Corda 4:

  • Package sealing
  • Package namespace ownership

Sealing. App isolation has been improved. Version 4 of the finance CorDapps (corda-finance-contracts.jar, corda-finance-workflows.jar) is now built as a set of sealed and signed JAR files. This means classes in your own CorDapps cannot be placed under the following package namespace: net.corda.finance

In the unlikely event that you were injecting code into net.corda.finance.* package namespaces from your own apps, you will need to move them into a new package, e.g. net/corda/finance/flows/MyClass.java can be moved to com/company/corda/finance/flows/MyClass.java. As a consequence your classes are no longer able to access non-public members of finance CorDapp classes.

When signing your JARs for Corda 4, your own apps will also become sealed, meaning other JARs cannot place classes into your own packages. This is a security upgrade that ensures package-private visibility in Java code works correctly. If other apps could define classes in your own packages, they could call package-private methods, which may not be expected by the developers.

Namespace ownership. This part is only relevant if you are joining a production compatibility zone. You may wish to contact your zone operator and request ownership of your root package namespaces (e.g. com.megacorp.*), with the signing keys you will be using to sign your app JARs. The zone operator can then add your signing key to the network parameters, and prevent attackers defining types in your own package namespaces. Whilst this feature is optional and not strictly required, it may be helpful to block attacks at the boundaries of a Corda based application where type names may be taken “as read”.

In Corda 4 it is possible for flows in one app to subclass and take over flows from another. This allows you to create generic, shared flow logic that individual users can customise at pre-agreed points (protected methods). For example, a site-specific app could be developed that causes transaction details to be converted to a PDF and sent to a particular printer. This would be an inappropriate feature to put into shared business logic, but it makes perfect sense to put into a user-specific app they developed themselves.

If your flows could benefit from being extended in this way, read “Configuring Responder Flows” to learn more.

In Corda 4 queries made on a node’s vault can filter by the relevancy of those states to the node. As this functionality does not exist in Corda 3, apps will continue to receive all states in any vault queries. However, it may make sense to migrate queries expecting just those states relevant to the node in question to query for only relevant states. See API: Vault Query for more details on how to do this. Not doing this may result in queries returning more states than expected if the node is using observer functionality (see “Posting transactions to observer nodes”).

Corda 4 adds several new APIs that help you build applications. Why not explore:

Please also read the CorDapp Upgradeability Guarantees associated with CorDapp upgrading.

If your project is based on one of the official cordapp templates, it is likely you have a lib/quasar.jar checked in. It is worth noting that you only use this if you use the JUnit runner in IntelliJ. In the latest release of the cordapp templates, this directory has been removed.

You have some choices here:

  • Upgrade your quasar.jar to 0.7.12_r3
  • Delete your lib directory and switch to using the Gradle test runner

Instructions for both options can be found in Running tests in Intellij.

Was this page helpful?

Thanks for your feedback!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.

We're sorry this page wasn't helpful. Let us know how we can make it better!

Chat with us

Chat with us on our #docs channel on slack. You can also join a lot of other slack channels there and have access to 1-on-1 communication with members of the R3 team and the online community.

Create an issue

Create a new GitHub issue in this repository - submit technical feedback, draw attention to a potential documentation bug, or share ideas for improvement and general feedback.

Propose documentation improvements directly

Help us to improve the docs by contributing directly. It's simple - just fork this repository and raise a PR of your own - R3's Technical Writers will review it and apply the relevant suggestions.