はじめに
本稿について
本稿は、CordaのToken開発キットである「Token SDK」に関して概要や実装方法について説明するものです。Token SDKを正しく使う事で独自Tokenを簡単に生成、使用することができます。
また、本稿はR3が提供しているToken SDKのトレーニング「You use the Tokens SDK」および「Refactor Solution」に沿った内容になっています。
実装
State
独自のエアマイルトークンを作成します。class「TokenType」を拡張することで、簡単にオリジナルのTokenを生成できます。以下の例は、Tokenの識別子が「AIR」、小数点以下が0桁の「AirMileType」を定義しています。
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);
}
}
また、実際にFlow上で使用する際は、FungibleTokenにてトークンのインスタンスを生成しますが、第三引数にTokenのjarのハッシュ値を渡すことができます。なので、AirMileTypeのメソッドに自身のjarのハッシュ値を取得するためのメソッドを実装してみましょう。加えて、equalsメソッドやhashCodeメソッドを実装して、AirMileTypeをよりセキュアなコードにしてみます。
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");
}
}
Contract
「AirMileType」はあくまでTokenの種類を定義したもので、厳密にはStateではありません。そのため、「AirMileType」に紐づくContractを定義する必要はありません。(Contractで検証されるべきは、FungibleTokenやNonFungibleTokenであり、TokenTypeはそれらの中に組み込まれる情報の一つです。)
しかし、Cordaの仕様には、「Contractコードを含めないcontract.jarはCorDapps起動時にロードしない」という物があります。そのため、ここではDummyContractを定義します。
public class DummyContract implements Contract {
private DummyContract() {
throw new NotImplementedException("This contract is not to be used");
}
@Override
public void verify(@NotNull LedgerTransaction tx) throws IllegalArgumentException {
throw new NotImplementedException("This contract is not to be used");
}
}
Contract Tests
エアマイルトークンのコントラクトコードは、FungibleTokenContractやNonFungibleTokenContractであり、厳密にはテスト対象のContractはないですが、Transactionの作成などを確認するにはテストコードを用いて検証することは有用です。ここでは、テストコードでダミーのTransactionを作成する方法について、説明します。
- テストServicesHubの生成
MockServicesを用いてテスト用のServiceHubを生成し、簡単なテストを実行することができます。パラメータとして任意のCorDappsパッケージを渡すことで、テスト対象のCorDappsを読み込みます。
private final MockServices ledgerServices = new MockServices(Collections.singletonList("com.r3.corda.lib.tokens.contracts"));
- テストPartyの生成
TestIdentityクラスを用いて、渡したパラメータより一時的なPartyを生成します。
private final Party alice = new TestIdentity(new CordaX500Name("Alice", "London", "GB")).getParty();
- テストTransactionを生成
作成したMockおよびPartyを用いてテストTransactionを実行し、検証します。
以下の例は、簡単なIssueTokenのテストを試みているものです。
(失敗するケースと成功するケース2種類のテストを実施しています)
@Test
public void transactionMustIncludeTheAttachment() {
transaction(ledgerServices, tx -> {
tx.output(FungibleTokenContract.Companion.getContractId(), create(aliceMile, bob, 10L));
tx.command(alice.getOwningKey(), new IssueTokenCommand(aliceMile, Collections.singletonList(0)));
tx.failsWith("Contract verification failed: Expected to find type jar"); // Contract Codeがattachmentに付与されていないため「期待通り」失敗する。
tx.attachment("com.template.contracts", AirMileType.getContractAttachment());
tx.verifies(); //検証に「期待通り」成功する。
return null;
});
} - tweakを用いた複数ケースのテスト実施
tweakはさらにローカルにスコープされたTransactionを生成することができます。元のTransactionに影響を加えることなく、分岐したテストコードを実施することが可能です。
@Test
public void transactionMustIncludeATokenContractCommand() {
transaction(ledgerServices, tx -> {
tx.attachment("com.template.contracts", AirMileType.getContractAttachment());
tx.output(FungibleTokenContract.Companion.getContractId(), create(aliceMile, bob, 10L));
tx.tweak(txCopy -> { // ローカルに分岐したTransactionを生成。ここで設定した値は、ローカル外に影響を及ぼさない。
// Let's add a command from an unrelated dummy contract.
txCopy.command(alice.getOwningKey(), new DummyContract.Commands.Create()); // ダミーコマンドを設定。
txCopy.failsWith("There must be at least one token command in this transaction"); // 期待通り失敗
return null;
});
tx.command(alice.getOwningKey(), new IssueTokenCommand(aliceMile, Collections.singletonList(0))); // 正しいコマンドを設定。
tx.verifies(); // 期待通り成功
return null;
});
}
Flow
Token SDKのモジュールを使用して以下の3つのFlowを作成します。
IssueFlows: subflowとして、IssueTokensを実行します。ちなみにIssueTokens内でResponderのIssueTokensHandlerを呼び出すため、ユーザー実装としてResponderを実装する必要はありません。
MoveFlows: subflowとして、AbstractMoveTokensFlowを実行します。なお、AbstractMoveTokensFlowは抽象メソッドのため、overrideした継承クラスを中で生成します。こちらもAbstractMoveTokensFlow内で相手Nodeを呼び出しているためResponderを実装する必要はありません。
return subFlow(new AbstractMoveTokensFlow() { // 抽象メソッドAbstractMoveTokensFlow
@NotNull
@Override
public List<FlowSession> getParticipantSessions() {
// 参加者の実装。AbstractMoveTokensFlowでは参加は定義されていないため、Tokenの関係者を定義することができます。
return participantSessions;
}
@NotNull
@Override
public List<FlowSession> getObserverSessions() {
// Observer(観察者)を定義することができます。今回は観察者無しとして定義しています。
return Collections.emptyList();
}
@NotNull
@Override
public ProgressTracker getProgressTracker() {
// ProgressTrackerを定義します。こちらに任意のTrackerを渡すことができます。
return PASSING_TO_SUB_MOVE.childProgressTracker();
}
@Override
public void addMove(@NotNull TransactionBuilder transactionBuilder) {
// 抽象メソッドaddMoveをoverride
MoveTokensUtilitiesKt.addMoveTokens(transactionBuilder, inputTokens, outputTokens);
}
});
RedeemFlows: subflowとして、RedeemTokensFlowを実行します。こちらもRedeemTokensFlow内でResponderのRedeemTokensHandlerを呼び出すため、ユーザー実装としてResponderを実装する必要はありません。
Flow Tests
Flowのテストを行うために、疑似ネットワークを構築してテストを実施することができます。
- テストネットワークの定義
テストネットワークの定義するためにはParameterやコンフィグの代わりにオブジェクト「MockNetworkParameters」を使用します。「FlowTestHelpers」には以下のとおりに定義されています。
@NotNull
上記MockNetworkParametersを使用したネットワークを構築します。
static MockNetworkParameters prepareMockNetworkParameters() throws Exception {
final Map<String, String> tokensConfig = getPropertiesFromConf("res/tokens-workflows.conf");
return new MockNetworkParameters()
// Notaryを定義
.withNotarySpecs(ImmutableList.of(
new MockNetworkNotarySpec(CordaX500Name.parse("O=Unwanted Notary, L=London, C=GB")),
new MockNetworkNotarySpec(CordaX500Name.parse(tokensConfig.get("notary")))))
// Nodeに含めるCorDappsを定義
.withCordappsForAllNodes(ImmutableList.of(
TestCordapp.findCordapp("com.r3.corda.lib.tokens.contracts"),
TestCordapp.findCordapp("com.r3.corda.lib.tokens.workflows")
.withConfig(tokensConfig),
TestCordapp.findCordapp("com.r3.corda.lib.tokens.money"),
TestCordapp.findCordapp("com.r3.corda.lib.tokens.selection"),
TestCordapp.findCordapp("com.template.states"),
TestCordapp.findCordapp("com.template.flows")));
}
private final MockNetwork network;
private final StartedMockNode alice;
private final StartedMockNode bob;
private final StartedMockNode carly;
private final StartedMockNode dan;
public IssueFlowsTests() throws Exception {
// prepareMockNetworkParameters = MockNetworkParameters
network = new MockNetwork(prepareMockNetworkParameters());
// createNodeは任意のNodeを生成してNode情報を返却します。
alice = network.createNode();
bob = network.createNode();
carly = network.createNode();
dan = network.createNode();
}
作成したネットワークでFlowTestを実行します。
@Test
public void signedTransactionReturnedByTheFlowIsSignedByTheIssuer() throws Exception {
// 実行するFlowを生成
final IssueFlows.Initiator flow = new IssueFlows.Initiator(bob.getInfo().getLegalIdentities().get(0), 10L);
// alice NodeからFlow実行。これを実行するとFlowのメッセージがキューに受信します。
final CordaFuture<SignedTransaction> future = alice.startFlow(flow);
// ネットワークを実行します。ここで取引が実行されます。
network.runNetwork();
// Transactionが返却されます。
final SignedTransaction tx = future.get();
// Transactionに必要な署名があることを検証します。
tx.verifyRequiredSignatures();
}
おわりに
基本的なToken SDKの拡張について学習しました。次回は、EvolvableTokenTypeとNonFungibleTokensについて学習します。
Created by: Kazuto Tateyama
Last edited by: Kazuto Tateyama
Updated: 2021/02/19