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 FungibleToken
s.
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.
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
, IssuedTokenType
s 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.
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:
- The
IssueFlows.Initiator
:- Sub-flows
IssueTokens
, which has its auto-initiated handler:IssueTokensHandler
. - Therefore does not need to be
@Initiating
.
- Sub-flows
- The
MoveFlows.Initiator
:- Sub-flows
AbstractMoveTokensFlow
, which is not@Initiating
and has theMoveTokensFlowHandler
. - Therefore needs to be
@Initiating
. - Because it will be used in tests only, it will be registered for auto-initiation in tests only.
- Sub-flows
- The
RedeemFlows.Initiator
:- Sub-flows
RedeemTokensFlow
, which is not@Initiating
and has theRedeemTokensFlowHandler
. - Therefore needs to be
@Initiating
. - Because it will be used in tests only, it will be registered for auto-initiation in tests only.
- Sub-flows
Multiple signatures
- The
IssueFlows
was configured to issue from a single issuer. This is also whatIssueTokens
does, so no big change here. - The
MoveFlows
was ready to move tokens from multiple holders. However,AbstractMoveTokensFlow
does not allow that, so you have to restrictMoveFlows
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. - 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:
You want to include all of your CorDapps, hence the long list of
TestCordapp.findCordapp
.You want to reuse the configuration file, instead of having the hard-coded preferred notary, which explains the
getPropertiesFromConf
andremoveQuotes
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 FungibleToken
s and flows, you will be speaking the same language as other CorDapps, which facilitates interoperability.
Time to move on to EvolvableTokenType
and NonFungibleToken
s.