5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

分散型台帳 Scalar DLT のスマートコントラクトの書き方を調べてみた

Last updated at Posted at 2019-07-22

今回はスマートコントラクトの書き方や次回以降で利用予定のサンプルアプリケーションの解説を行いたいと思います。
なお、前回までの記事は以下の通りです。

資産管理アプリケーション

サンプルアプリケーションですが、以前チラッとご紹介した以下のものを使いたいと思います。

こちらのアプリケーションは非常にシンプルに資産を管理するもので、スマートコントラクトは以下の6つが用意されています。

コントラクト名 機能
AddTypeContract 資産の種類の登録を行う
ListTypeContract 資産の種類の一覧を取得する
AddAssetContract 資産の登録を行う
ListContract 資産の一覧を取得する
StateChangeContract 資産の状態を変更する
AssetHistoryContract 資産の利用履歴を取得する

このスマートコントラクトを用いて資産の登録・閲覧・借用・返却が出来るようになっています。
早速各ソースの中身をご紹介したいところなのですが、その前にスマートコントラクトの基本からご説明しましょう。

スマートコントラクトの書き方

Scalar DLのスマートコントラクトはピュアJavaでの記述が可能です。
とはいえ好き勝手に書いてしまうのはよろしくないのでガイドラインをご紹介します。

  • 1つのスマートコントラクト内に複雑なロジックを含めないで下さい。複雑なロジックを扱いたい場合はロジックを分解して、ネストした呼び出しをしましょう。
  • Scalar DLTライブラリ以外の外部Javaライブラリの使用は許可されていません。
  • 内部クラスは許可されています。
  • ネストされたスマートコントラクトの呼び出しが可能です。
  • invoke() メソッドを必ず実装する必要があります。

外部ライブラリが使えないという制限は有りますが、スマートコントラクト側でそこまで特異な処理をさせなければいいだけなので特に問題にはならないでしょう。
ではベースとなるコードも見ていきましょう。

import com.scalar.ledger.contract.Contract;
import com.scalar.ledger.ledger.Ledger;
import java.util.Optional;
import javax.json.Json;
import javax.json.JsonObject;

public class MyContract extends Contract {
  @Override
  public JsonObject invoke(Ledger ledger, JsonObject argument, Optional<JsonObject> property) {
    // read or write data and respond to clients here
  }
}

invoke()メソッドだけが記述されていますが、こちらが実際に呼び出される部分なので必ず実装する必要があります。
引数のledgerはScalar DBに保存されるKey-Value型のデータにアクセスするためのオブジェクトになります。データの追加・更新・読取・取得が可能です。
argumentはスマートコントラクトに渡されたパラメータでJSON形式になっています。このオブジェクトにはnonceという文字列型のプロパティを設定するのが推奨されています。世界で一意になるような値が良いとの事なのでUUID辺りを使用するのが良いと思います。
propertyはスマートコントラクトのデプロイ時に設定されたパラメータになります。こちらは後からの変更は不可能です。

ledgerのメソッドに関しても少し解説しましょう。
まずはOptional<Asset> get(String key)ですが、これは指定したキーの現在の値を取得するために使われます。

Optional<Asset> opt = ledger.get("somekey");
if (opt.isPresent()) {
  Asset asset = opt.get();
  int age = asset.age();
  JsonObject value = asset.data();
}

ここで取得できるageはデータが更新された回数で、0から自動でカウントされていきます。
続いてはvoid put(String key, JsonObject value)についてですが、これは指定したキーでJSONオブジェクトを登録します。

Optional<Asset> opt = ledger.get(name);
if (opt.isPresent()) {
  return Json.createObjectBuilder()
    .add("message", "Type " + name + " is already registered.")
    .build();
}

ledger.put(name, Json.createObjectBuilder().build());

putは暗黙的な書き込みは許可されていないので、必ずgetしてから書き込むようにしてください。
続いてList<Asset> scan(String key)についてです。これは指定したキーの変更履歴を取得できます。

AssetFilter filter = new AssetFilter(key);
List<Asset> history = ledger.scan(filter);
for (Asset asset : history) {
  String id = asset.id();
  int age = asset.age();
  JsonObject value = asset.data();
}

急にAssetFilterというのが出てきましたが、こちらは履歴の範囲指定が出来るクラスになります。

// age が 5 から 9 まで
new AssetFilter(id).withStartVersion(5, true).withEndVersion(10, false);
// age が 5 から最新まで
new AssetFilter(id).withStartVersion(5, true);

と、ここまでが基本的な部分です。
ちょっと長くなりましたが、これを押さえておけばスマートコントラクトの記述は簡単に感じられると思います。

資産管理アプリケーションのスマートコントラクト

AddTypeContract

AddTypeContract は資産の種類を登録する処理になります。
ソースの全体はこちらでご確認いただけるので、ここではポイントを抽出して解説していきたいと思います。

  public static final String HOLDER_ID = "holderId";

まずはこの資産管理アプリケーション全体で重要なプロパティです。
各スマートコントラクトではholderIdというプロパティを持っており、これはデプロイ時にしか設定が出来ません。

    if (!property.isPresent() || !property.get().containsKey(HOLDER_ID)) {
      throw new ContractContextException("property: `" + HOLDER_ID + "` is mandatory.");
    }

スマートコントラクトが呼び出されたときに、上記のようにプロパティをチェックする事でデプロイしたユーザ以外の実行を防いでいます。
あとは単純で同一名称のチェックを行っています。

    Optional<Asset> type = ledger.get(holderId + "-" + name);
    if (type.isPresent()) {
      return Json.createObjectBuilder()
          .add(RESULT, FAILURE)
          .add(MESSAGE, "Type " + name + " is already registered.")
          .build();
    }

    ledger.put(holderId + "-" + name, Json.createObjectBuilder().build());

そして最後にholderId + "-" + TYPEのkeyのvalueに資産の種類を追加しています。

    JsonObject newType = Json.createObjectBuilder().add(NAME, name).build();
    ledger.get(holderId + "-" + TYPE);
    ledger.put(holderId + "-" + TYPE, newType);

ListTypeContract

ListTypeContract は登録された資産の種類の一覧を返す処理になります。
ソースはこちらになります。

holderIdの処理は共通なので説明を省くと、データの取得だけなので処理は単純です。

    AssetFilter filter = new AssetFilter(holderId + "-" + TYPE);
    List<Asset> history = ledger.scan(filter);
    if (history.isEmpty()) {
      return Json.createObjectBuilder()
          .add(RESULT, FAILURE)
          .add(MESSAGE, "No types were registered. Use am add-type to create one.")
          .build();
    }

    JsonArrayBuilder builder = Json.createArrayBuilder();
    for (Asset h : history) {
      JsonObject type =
          Json.createObjectBuilder().add(TYPE, h.data().getString(NAME)).add(AGE, h.age()).build();
      builder.add(type);
    }
    JsonArray types = builder.build();

データが無ければエラーを返し、データが有れば配列にして返す形ですね。

AddAssetContract

お次の AddAssetContract は AddTypeContract に似ていますが、種類を指定して資産を登録する処理になっています。
ソースはこちらです。

基本的には AddTypeContract と変わりませんが、必須な引数が多いですね。

    if (!argument.containsKey(TYPE)
        || !argument.containsKey(ASSET)
        || !argument.containsKey(TIMESTAMP)
        || !argument.containsKey(ID)) {
      throw new ContractContextException("wrong argument.");
    }

種類と資産の存在チェックは説明を省きますが、その後に貸し出し状態と名前を保存していますね。

    JsonObject assetStatusJson =
        Json.createObjectBuilder().add(TIMESTAMP, timestamp).add(STATUS, IN_STOCK).build();
    ledger.put(holderId + "-" + id, assetStatusJson);

    JsonObject assetNameJson = Json.createObjectBuilder().add(ID, id).add(NAME, name).build();
    ledger.put(holderId + "-" + type, assetNameJson);

ListContract

ListContract もご想像通り、ListTypeContract と基本的には変わりません。
そーすはこちらになります。

ListTypeContract に比べてtypeという引数が必須になっており、指定されたtypeの資産一覧を返す形です。
少し処理が長くなっているのは、貸し出し状態を別処理で取得している関係ですね。

    JsonArrayBuilder assetsBuilder = Json.createArrayBuilder();
    for (Asset asset : assetList) {
      JsonObject data = asset.data();
      if (data.size() == 0) { // initiated one, ignore it
        continue;
      }
      String id = data.getString(ID);
      String name = data.getString(NAME);

      Optional<Asset> borrowingStatus = ledger.get(holderId + "-" + id);
      if (!borrowingStatus.isPresent()) {
        /**
         * Abnormal case. We found an asset in list but no borrowing status record. Just ignore it
         */
        continue;
      }

      JsonObjectBuilder statusBuilder = Json.createObjectBuilder();
      statusBuilder
          .add(ID, id)
          .add(NAME, name)
          .add(TIMESTAMP, borrowingStatus.get().data().getJsonNumber(TIMESTAMP).longValue())
          .add(STATUS, borrowingStatus.get().data().getString(STATUS));

      if (borrowingStatus.get().data().containsKey(HOLDER_ID)) {
        statusBuilder.add(HOLDER_ID, borrowingStatus.get().data().getString(HOLDER_ID));
      }

      assetsBuilder.add(statusBuilder.build());
    }

StateChangeContract

続いての StateChangeContract はこのアプリケーションのメイン部分ですね。
借用・返却のステータス管理を担当します。
ソースはこちらです。

メインと言っても処理は単純で、まずは現在と同じステータスが指定された場合はエラーを返しています。

    if (data.getString(STATUS).equals(newStatus)) {
      return Json.createObjectBuilder()
          .add(RESULT, FAILURE)
          .add(
              MESSAGE,
              String.format(
                  "Asset is already %s.", ON_LOAN.equals(newStatus) ? "borrowed" : "returned"))
          .build();
    }

あとは借用・返却時のステータス変更の処理になります。

    JsonObjectBuilder newDataBuilder = Json.createObjectBuilder();
    if (newStatus.equals(ON_LOAN)) {
      newDataBuilder.add(HOLDER_ID, holderId).add(TIMESTAMP, timestamp).add(STATUS, newStatus);
    } else if (newStatus.equals(IN_STOCK)) {
      if (!data.containsKey(HOLDER_ID)) {
        return Json.createObjectBuilder()
            .add(RESULT, FAILURE)
            .add(MESSAGE, "Can not return asset without holderId")
            .build();
      }
      if (!data.getString(HOLDER_ID).equals(holderId)) {
        return Json.createObjectBuilder()
            .add(RESULT, FAILURE)
            .add(MESSAGE, "Can not return asset borrowed by another user")
            .build();
      }
      newDataBuilder.add(TIMESTAMP, timestamp).add(STATUS, newStatus);
    }
    ledger.put(holderId + "-" + id, newDataBuilder.build());

ここではholderIdを用いてチェックを行い、ユーザが違う場合は警告メッセージも出しています。

AssetHistoryContract

最後の AssetHistoryContract は資産のIDを指定することで今までの貸し出し履歴を返す処理です。
ソースはこちらになります。

履歴が無い時はエラーを返す仕様です。

    AssetFilter filter = new AssetFilter(holderId + "-" + id);
    List<Asset> borrowingHistory = ledger.scan(filter);

    if (borrowingHistory.isEmpty()) {
      return Json.createObjectBuilder()
          .add(RESULT, FAILURE)
          .add(MESSAGE, "This asset is not registered")
          .build();
    }

履歴が有れば配列にして返します。

    JsonArrayBuilder builder = Json.createArrayBuilder();
    JsonObjectBuilder borrowingRecordBuilder = Json.createObjectBuilder();
    for (Asset history : borrowingHistory) {
      borrowingRecordBuilder
          .add(TIMESTAMP, history.data().getJsonNumber(TIMESTAMP).longValue())
          .add(STATUS, history.data().getString(STATUS))
          .add(AGE, history.age());

      if (history.data().containsKey(HOLDER_ID)) {
        borrowingRecordBuilder.add(HOLDER_ID, history.data().getString(HOLDER_ID));
      }
      builder.add(borrowingRecordBuilder.build());
    }

まとめ

というわけでスマートコントラクトの書き方と資産管理アプリケーションのソースの解説でした。
次回からはこのソースを用いてエミュレータの使い方などに触れていければと思います。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?