Token SDKの実装について③(Token SDKを使ってCorDappsを改良する 実装)

はじめに

本稿について

本稿は、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
    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")));
    }
    上記MockNetworkParametersを使用したネットワークを構築します。
    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

この記事は役に立ちましたか?
0人中0人がこの記事が役に立ったと言っています