/
Launch Online IDE

Refactor Solution

Example project using the token SDK


You will get the most from this example if you compare to your own attempt. Did you refactor your project to use the SDK? Compare this example to your attempt. The code can be found here, and in IntelliJ, you need to import the 030-tokens-sdk folder as a project.

You will notice more than bland replacements. There are some extra learning nuggets in here.

constants.properties & build.gradle

In the constants, notice the versions that will be used:

tokensReleaseVersion=1.1
tokensReleaseGroup=com.r3.corda.lib.tokens
confidentialIdReleaseVersion=1.0
confidentialIdReleaseGroup=com.r3.corda.lib.ci

The FungibleToken takes an AbstractParty holder, and the flows can handle anonymous parties, so the confidential app is added as well.

There are 3 build.gradle files:

  • The root one:

    • Where you add the constants definition in buildscript.ext:

      tokens_release_version = constants.getProperty("tokensReleaseVersion")
      tokens_release_group = constants.getProperty("tokensReleaseGroup")
      confidential_id_release_version = constants.getProperty("confidentialIdReleaseVersion")
      confidential_id_release_group = constants.getProperty("confidentialIdReleaseGroup")
    • The dependencies:

      // Token SDK dependencies.
      cordapp "$confidential_id_release_group:ci-workflows:$confidential_id_release_version"
      cordapp "$tokens_release_group:tokens-contracts:$tokens_release_version"
      cordapp "$tokens_release_group:tokens-workflows:$tokens_release_version"
      cordapp "$tokens_release_group:tokens-money:$tokens_release_version"
      cordapp "$tokens_release_group:tokens-selection:$tokens_release_version"
    • For completeness, the deployNodes.nodeDefaults:

      cordapp project(':contracts')
      cordapp (project(':workflows')) {
          config project.file("res/tokens-workflows.conf") // About this in an moment
      }
      // Token SDK dependencies.
      cordapp "$confidential_id_release_group:ci-workflows:$confidential_id_release_version"
      cordapp "$tokens_release_group:tokens-contracts:$tokens_release_version"
      cordapp "$tokens_release_group:tokens-workflows:$tokens_release_version"
      cordapp "$tokens_release_group:tokens-money:$tokens_release_version"
      cordapp "$tokens_release_group:tokens-selection:$tokens_release_version"
    • Explicitly clear the notary node:

      cordapps.clear()
  • For contracts, only the dependencies need updating:

    cordapp "$tokens_release_group:tokens-contracts:$tokens_release_version"
    cordapp "$tokens_release_group:tokens-money:$tokens_release_version"
  • For workflows, also only dependencies:

    cordapp "$confidential_id_release_group:ci-workflows:$confidential_id_release_version"
    cordapp "$tokens_release_group:tokens-workflows:$tokens_release_version"
    cordapp "$tokens_release_group:tokens-selection:$tokens_release_version"

The air-mile type

The first decision is how to agree on a common TokenType for the air-mile. Remember, this is not the issued type, it is the base type. The first idea that will work is to do something like:

public final class AirMileType extends TokenType {

    public static final String IDENTIFIER = "AIR";
    public static final int FRACTION_DIGITS = 0;

    public static TokenType create() {
        return new TokenType(IDENTIFIER, FRACTION_DIGITS);
    }
}

Why use a static constructor and not make it a proper class? Good question. This is to avoid having to handle complications surrounding FungibleToken. Skipped is the detail that the FungibleToken constructor wants the hash of the JAR file that hosts your token type. With the static constructor, lazily doing:

new FungibleToken(amount, alice, null)

This will work without issue. However, you should not be completely satisfied with this half-way measure. Instead, while waiting for @JvmOverloads to make it into a release, you can be explicit about the jar hash:

public final class AirMileType extends TokenType {

    public static final String IDENTIFIER = "AIR";
    public static final int FRACTION_DIGITS = 0;

    @NotNull
    public static SecureHash getContractAttachment() {
        //noinspection ConstantConditions
        return TransactionUtilitiesKt.getAttachmentIdForGenericParam(new AirMileType());
    }

    public AirMileType() {
        super(IDENTIFIER, FRACTION_DIGITS);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        return o != null && getClass() == o.getClass();
    }

    @Override
    public int hashCode() {
        return Objects.hash("AirMileType");
    }
}

Of course, do not forget the hashCode and equals functions that ensure an AirMileType instance is no different from another. This getContractAttachment() function is the one you will need to use when instantiating FungibleTokens.

You will notice a DummyContract, to satisfy the basics of a "Contracts" CorDapp.

The contract learning tests

The previous tests on TokenContract are no longer needed.

tip icon

The point here isn't to create unit tests on the SDK's contracts. Those belong to the SDK's repo itself. However, unit tests not only confirm features and detect regressions, they also describe how to use your code, in this case, how to create transactions with AirMileType.

If you compare with the tests you created earlier, you had to make the following adjustments.

Setting up the mocks

It got a wee bit more involved. Explicitly add the tokens contracts:

private final MockServices ledgerServices = new MockServices(Collections.singletonList("com.r3.corda.lib.tokens.contracts"));

Declutter

Since there are a few more steps in order to create a FungibleToken, IssuedTokenTypes are pre-created:

private final IssuedTokenType aliceMile = new IssuedTokenType(alice, new AirMileType());
private final IssuedTokenType carlyMile = new IssuedTokenType(carly, new AirMileType());

And a function to encapsulate some complexity:

@NotNull
private FungibleToken create(
        @NotNull final IssuedTokenType tokenType,
        @NotNull final Party holder,
        final long quantity) {
    return new FungibleToken(new Amount<>(quantity, tokenType), holder, AirMileType.getContractAttachment());
}

Notice the use of the AirMileType.getContractAttachment() argument.

The attachment

Because the Jar hash is specified for AirMileType, the attachment itself is pretend-added, which is why you see this new line repeated in each test:

tx.attachment("com.template.contracts", AirMileType.getContractAttachment());

And in order to confirm that this is indeed necessary, an added test @Test transactionMustIncludeTheAttachment().

tx.tweak?

Oh yes, now is a good time to learn this technique. It clones the tx and gives you the copy inside the argument lambda. With this copy, you can perform some extra tests, and when you exit the lambda, you are back to the previous transaction. A good use case is to make sure you are testing what you think you are testing. You see, when you run the following (pseudo-code) test:

tx.input(a, b);
tx.command(c, d);
tx.failsWith("bad, try again");

Did you 100% check that only the input was wrong? Or was it the command? Or both? Or was it a, b, c or d or a combination of them? Something else? In comes tweak where you can make sure it is d in the command that is wrong:

tx.input(a, b);
tx.tweak(txCopy -> {
    txCopy.command(c, d); // <- txCopy!
    txCopy.failsWith("bad, try again");
    return null;
});
// At this point, tx has no knowledge of what happened inside tweak.
tx.command(c, e); // <- tx, and we changed only d to e
tx.verifies();

Isn't it now obvious that it was d in the command that was the problem? Since having e as the only difference made the transaction verify, and the 2 are a few lines apart; even fewer if you are using Kotlin.

tip icon

Thanks to tweak you have a high assurance that the error you thought you tested is what you tested indeed. Having it all in a single test is more encapsulated than having 2 individual tests testing both scenarios. Those 2 individual tests might be modified independently by a developer who may not fully grasp the connection between the 2.

Multiple issuers

The old TokenContract allowed token issuance from multiple issuers with a single command:

tx.command(Arrays.asList(alice.getOwningKey(), carly.getOwningKey()), new TokenContract.Commands.Issue());

With the Tokens SDK, you can still issue from multiple issuers, although you have to add 1 command per issuer:

tx.command(alice.getOwningKey(), new IssueTokenCommand(aliceMile, Arrays.asList(0, 1, 2)));
tx.command(carly.getOwningKey(), new IssueTokenCommand(carlyMile, Arrays.asList(3, 4)));

The same concept applies for move and redeem.

0 in input quantity?

In TokenContract, quantity 0 is prevented in all situations. However, the Tokens SDK allows this situation in inputs only. The rationale being that you may want to mop up bad states, which are still impossible to create. So you have to change your tests to accommodate for this change of specification: @Test inputsMayHaveAZeroQuantity().

Redeem with outputs?

Here too the specifications have changed, and tests need to reflect that: @Test redeemTransactionMustHaveLessInOutputs(). The outputs are understood as the change (as in "Do you have change for a $20 bill?") given when redeeming states with a quantity larger than desired. Remember the RedeemFlows.SimpleInitiator? It did a move transaction, in order to reorganize the states, before a redeem if the collected quantity was too high. Here, with the change mechanism, the Tokens SDK allows for a single redeem transaction. This requires some attention but is on the whole more elegant.

The flows

Config file

Do you remember the preferred notary? It was hard-coded. Now there is new a configuration file in res/tokens-workflow.conf. This is the file that deployNodes.nodeDefaults is instructed to pick when setting up the Tokens CorDapp. It only contains:

notary="O=App Notary,L=London,C=GB"

@Initiating and @InitiatedBy

First thing you will notice is that the responder flows are removed. Each refactored flow is only doing local actions before calling subFlow, once, on an existing flow of the Tokens SDK. So the responding actions are already defined. In detail:

  1. The IssueFlows.Initiator:
    • Sub-flows IssueTokens, which has its auto-initiated handler: IssueTokensHandler.
    • Therefore does not need to be @Initiating.
  2. The MoveFlows.Initiator:
    • Sub-flows AbstractMoveTokensFlow, which is not @Initiating and has the MoveTokensFlowHandler.
    • Therefore needs to be @Initiating.
    • Because it will be used in tests only, it will be registered for auto-initiation in tests only.
  3. The RedeemFlows.Initiator:
    • Sub-flows RedeemTokensFlow, which is not @Initiating and has the RedeemTokensFlowHandler.
    • Therefore needs to be @Initiating.
    • Because it will be used in tests only, it will be registered for auto-initiation in tests only.

Multiple signatures

  1. The IssueFlows was configured to issue from a single issuer. This is also what IssueTokens does, so no big change here.
  2. The MoveFlows was ready to move tokens from multiple holders. However, AbstractMoveTokensFlow does not allow that, so you have to restrict MoveFlows to accommodate this. Of course, you can write a more complex flow for multiple holders, but as mentioned earlier, this is a potentially dangerous route, so that is left for a later chapter when a flow wants to do more than just move tokens.
  3. The RedeemFlows was ready to redeem tokens from multiple holders and issuers. However, here too, it is limited by the sub flows it calls and it is ok.

progressTracker

Alas, the SDK flows do not make it easy to pass a progress tracker, so it sort of drops the ball here, hoping a future version will accommodate this. Except on MoveFlows.Initiator where you can override the getter on AbstractMoveTokensFlow.

Flow tests

Setting up the mocks

There was some work here:

  1. You want to include all of your CorDapps, hence the long list of TestCordapp.findCordapp.

  2. You want to reuse the configuration file, instead of having the hard-coded preferred notary, which explains the getPropertiesFromConf and removeQuotes functions (which are not so important for the overall learning here) to arrive at:

    final Map<String, String> tokensConfig = getPropertiesFromConf("res/tokens-workflows.conf");

    which is used in:

    TestCordapp.findCordapp("com.r3.corda.lib.tokens.workflows")
                                .withConfig(tokensConfig)

    and in preparing the notaries. Notice the added notary, to be able to test that it gets the preferred one, and not just the first in the list:

    .withNotarySpecs(ImmutableList.of(
            new MockNetworkNotarySpec(CordaX500Name.parse("O=Unwanted Notary, L=London, C=GB")),
            new MockNetworkNotarySpec(CordaX500Name.parse(tokensConfig.get("notary")))))

The attachment?

This is taken care of by the sub flows it calls, so all good.

Refactor

Only a few changes were made such that it tests only what the flows can do, as explained above.

Conclusion

You have reviewed the example refactor, what was added, what was removed, and you learned about tx.tweak in the process. You have had your first glimpse at a CorDapp configuration file. Why go through the trouble of using someone else's work when you had a perfectly fine TokenState? Well, with this AirMileType, its FungibleTokens and flows, you will be speaking the same language as other CorDapps, which facilitates interoperability.

Time to move on to EvolvableTokenType and NonFungibleTokens.

Discuss on Slack
Rate this Page
Would you like to add a message?
Submit
Thank you for your Feedback!