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

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

- [最近話題の分散型台帳 Scalar DLT について調べてみた（基礎編）](https://qiita.com/DENEN_YOSHIDA/items/591a28ba8196d3fbda9a)
- [最近話題の分散型台帳 Scalar DLT について調べてみた（スマートコントラクト実行編）](https://qiita.com/DENEN_YOSHIDA/items/8642d99be2e7a1db87fa)

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

- [INDETAIL社によるScalar DL入門](https://github.com/indetail-blockchain/getting-started-with-scalardl)

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

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

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

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

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

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

```java
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)`ですが、これは指定したキーの現在の値を取得するために使われます。

```java
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オブジェクトを登録します。

```java
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)`についてです。これは指定したキーの変更履歴を取得できます。

```java
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`というのが出てきましたが、こちらは履歴の範囲指定が出来るクラスになります。

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

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

# 資産管理アプリケーションのスマートコントラクト
## AddTypeContract
AddTypeContract は資産の種類を登録する処理になります。
ソースの全体は[こちら](https://github.com/indetail-blockchain/getting-started-with-scalardl/blob/master/src/main/java/com/scalar/am/contract/AddTypeContract.java)でご確認いただけるので、ここではポイントを抽出して解説していきたいと思います。

```java
  public static final String HOLDER_ID = "holderId";
```

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

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

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

```java
    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に資産の種類を追加しています。

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

## ListTypeContract
ListTypeContract は登録された資産の種類の一覧を返す処理になります。
ソースは[こちら](https://github.com/indetail-blockchain/getting-started-with-scalardl/blob/master/src/main/java/com/scalar/am/contract/ListTypeContract.java)になります。

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

```java
    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 に似ていますが、種類を指定して資産を登録する処理になっています。
ソースは[こちら](https://github.com/indetail-blockchain/getting-started-with-scalardl/blob/master/src/main/java/com/scalar/am/contract/AddAssetContract.java)です。

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

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

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

```java
    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 と基本的には変わりません。
そーすは[こちら](https://github.com/indetail-blockchain/getting-started-with-scalardl/blob/master/src/main/java/com/scalar/am/contract/ListContract.java)になります。

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

```java
    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 はこのアプリケーションのメイン部分ですね。
借用・返却のステータス管理を担当します。
ソースは[こちら](https://github.com/indetail-blockchain/getting-started-with-scalardl/blob/master/src/main/java/com/scalar/am/contract/StatusChangeContract.java)です。

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

```java
    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();
    }
```

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

```java
    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を指定することで今までの貸し出し履歴を返す処理です。
ソースは[こちら](https://github.com/indetail-blockchain/getting-started-with-scalardl/blob/master/src/main/java/com/scalar/am/contract/AssetHistoryContract.java)になります。

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

```java
    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();
    }
```

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

```java
    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());
    }
```

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