ファクトリーパターンを使ったコントラクトの書き方

このガイドでは、シンプルなカウンターコントラクトのファクトリーパターンを用いて、このパターンをサポートするCasper APIを紹介します。このガイドのサンプルコントラクトは、ここにあるカウンターコントラクトを修正したものです。

ファクトリーパターンは、様々なプログラミングコンテキストで使用される、広く認知されたソフトウェア設計コンセプトです。DApp開発者は、ファクトリーメソッドやエントリーポイントなど、与えられたソース(またはファクトリー)からスマートコントラクトを作成するために、ファクトリー実装を使用できます。ファクトリーパターンは、生成されるコントラクトが特定のエントリポイントや引数など、指定された動作を維持することを保証します。一般的に、ファクトリーはテンプレートに従って他のスマートコントラクトを生成します。

Casperのファクトリーは、EntryPointType::Installと呼ばれるエントリーポイントタイプを使用して作成され、エントリーポイントをチェーン上のコントラクトの作成とインストールに対応するファクトリーメソッドとしてマークします。インストーラエントリポイントは、元のセッションWasmに基づいて新しいWasmを導出し、必要に応じて異なるエントリポイントのセットを持つ新しいコントラクトを作成します。言い換えると、EntryPointType::Installでマークされたインストーラエントリポイントは、コントラクトファクトリということです。このページで用いるファクトリーコントラクトとは、ファクトリーエントリポイントを含むコントラクトを意味しています。

EntryPointAccess::Templateは、バイトコードに存在するが呼び出し可能ではないエントリーポイントをマークします。したがって、通常のエントリーポイントは、EntryPointType::Installでマークされたインストーラのエントリーポイント内から参照できます。 オブジェクト指向の用語では、EntryPointAccess::Templateでマークされたエントリーポイントは、仮想抽象メソッドとして動作し、セッションコードから呼び出すことはできません。

💡NOTE

このファクトリーパターンは、Wasmを使用する際に既知の欠点をもたらします。ファクトリーパターンで作成されたすべてのスマートコントラクトは、チェーン上にインストールされた同じWasmを共有します。そのため、開発者は一度インストールしたWasmを変更したり、ファクトリーパターンを使って変更したコントラクトを作成したりすることはできません。開発者は、親コントラクトで可能なすべてのエントリーポイントを指定し、EntryPointAccess::Templateマーカーでタグ付けする必要があります。

カウンターファクトリーの例

このセクションでは、ファクトリーメソッドを使用するシンプルなカウンターを掘り下げて、Casper ネットワークでファクトリーパターンを実装する方法を説明します。Testnet上のカウンターコントラクトのチュートリアルは、ファクトリでないバージョンのカウンターコントラクトを示しています。

まず、コントラクトのエントリーポイントが定義されているセッションコードから見ていきましょう。

2つのインストーラエントリポイントは、EntryPointType::Installでマークされています。つまり、これらのエ ントリポイントは、このWasmがグローバルステートにインストールされると、新しいカウンターコントラクトを生成します。また、セッションコードから呼び出せるように、EntryPointAccess::Publicでマークされています。

let entry_point: EntryPoint = EntryPoint::new(
    CONTRACT_FACTORY_ENTRY_POINT.to_string(),
    Parameters::new(),
    CLType::Unit,
    EntryPointAccess::Public,
    EntryPointType::Install,
);
entry_points.add_entry_point(entry_point);
let entry_point: EntryPoint = EntryPoint::new(
    CONTRACT_FACTORY_DEFAULT_ENTRY_POINT.to_string(),
    Parameters::new(),
    CLType::Unit,
    EntryPointAccess::Public,
    EntryPointType::Install,
);

これらの2つのインストーラは、複数のファクトリー・エントリーポイントを宣言し、それらが生成するWasmを異なる値で初期化するために使用する方法を示しています。61行目contract_factoryエントリーポイントは、指定された名前と初期値を持つカウンターコントラクトを作成します。

#[no_mangle]
pub extern "C" fn contract_factory() {
    let name: String = runtime::get_named_arg(ARG_NAME);
    let initial_value: U512 = runtime::get_named_arg(ARG_INITIAL_VALUE);
    installer(name, initial_value);
}

68行目では、contract_factory_defaultエントリーポイントが、与えられた名前とゼロの初期値を持つカウンターコントラクトを作成しています。

#[no_mangle]
pub extern "C" fn contract_factory_default() {
    let name: String = runtime::get_named_arg(ARG_NAME);
    installer(name, U512::zero());
}

💡Note

ファクトリーパターンは、異なるエントリーポイントを持つコントラクトを生成することができます。セッションコードがエントリーポイント A、B、C、D をテンプレートとして定義しているとします。1つのインストーラファクトリーエントリーポイントは、エントリーポイントAとBを使用しコントラクトを生成でき、他のエントリーポイントは、エントリーポイントCとDを使用します。このようなAPIレベルのサポートにより、より複雑なユースケースの実装が可能になります。

インストーラ関数は、名前付きキーとエントリーポイントを指定することで、新しいカウンターコントラクトを作成します。名前付きキーにはカウンターの初期値が含まれ、エントリーポイントはカウンターのdecrementincrementの機能を定義します。これらのエントリーポイントは、他のスマートコントラクトと同様に、EntryPointAccess::PublicEntryPointType::Contractで定義され、作成されたすべてのカウンターに対して呼び出し可能となっています。increment関数とdecrement関数を呼び出す方法については、カウンターコントラクトの非ファクトリバージョンであるTestnet上のカウンターコントラクトチュートリアルを参照してください。

  • ▶カウンタファクトリのサンプルインストーラコード
    fn installer(name: String, initial_value: U512) {
        let named_keys = {
            let new_uref = storage::new_uref(initial_value);
            let mut named_keys = NamedKeys::new();
            named_keys.insert(CURRENT_VALUE_KEY.to_string(), new_uref.into());
            named_keys
        };
    
        let entry_points = {
            let mut entry_points = EntryPoints::new();
            let entry_point: EntryPoint = EntryPoint::new(
                INCREASE_ENTRY_POINT.to_string(),
                Parameters::new(),
                CLType::Unit,
                EntryPointAccess::Public,
                EntryPointType::Contract,
            );
            entry_points.add_entry_point(entry_point);
            let entry_point: EntryPoint = EntryPoint::new(
                DECREASE_ENTRY_POINT.to_string(),
                Parameters::new(),
                CLType::Unit,
                EntryPointAccess::Public,
                EntryPointType::Contract,
            );
            entry_points.add_entry_point(entry_point);
    
            entry_points
        };
    
        let (contract_hash, contract_version) = storage::new_contract(
            entry_points,
            Some(named_keys),
            Some(PACKAGE_HASH_KEY_NAME.to_string()),
            Some(ACCESS_KEY_NAME.to_string()),
        );
    
        runtime::put_key(CONTRACT_VERSION, storage::new_uref(contract_version).into());
        runtime::put_key(&name, contract_hash.into());
    }

インストーラロジックが、新しく作成されたコントラクトバージョンとコントラクトハッシュをファクトリコントラクトの名前付きキー下に保存することには十分な注意が必要です。インストーラロジックは、アカウントコンテキスト内で実行されるセッションコードの一部ではなく、ファクトリーコントラクトコンテキスト内で実行されます。詳細は、セッションとコントラクトコンテキストの比較を参照してください。

runtime::put_key(CONTRACT_VERSION, storage::new_uref(contract_version).into());
runtime::put_key(&name, contract_hash.into());

例えば、ファクトリーカウンターコントラクトをインストールした場合、このコントラクトの名前付きキーがアカウントに1つだけ表示され、2つのインストーラーエントリーポイントcontract_factorycontract_factory_defaultが表示されます。155-163行を参照してください。

3つの異なるカウンターを作成するためにインストーラを3回呼び出すと、ファクトリーコントラクトの名前付きキーに、各カウンターの名前付きキーが3つ表示されます。生成されたカウンターのコントラクトには、incrementdecrementのエントリーポイントがあります。上記で説明したように、開発者は、ファクトリーコントラクトで可能なすべての非インストーラエントリーポイントを定義し、EntryPointAccess::TemplateEntryPointType::Contractマーカーでタグ付けする必要があります。135~139行目を参照してください:

let entry_point: EntryPoint = EntryPoint::new(
    INCREASE_ENTRY_POINT.to_string(),
    Parameters::new(),
    CLType::Unit,
    EntryPointAccess::Template,
    EntryPointType::Contract,
);
entry_points.add_entry_point(entry_point);
let entry_point: EntryPoint = EntryPoint::new(
    DECREASE_ENTRY_POINT.to_string(),
    Parameters::new(),
    CLType::Unit,
    EntryPointAccess::Template,
    EntryPointType::Contract,
);

⚠注意

開発者が一番外側のセッションロジック (call関数) でエントリーポイントを宣言し、EntryPointAccess::Templateでマークするのを忘れた場合、ファクトリコントラクトがグローバルステートにインストールされると、そのWasmエクスポートは削除されます。インストーラロジックにエントリーポイントを作成するだけでは不十分です。

ユニットテスト

開発者は、「スマートコントラクトのユニットテスト」で説明したCasperテストフレームワークを使用して、ファクトリーパターンに従うコントラクトをテストできます。テストプロセスは同じですが、このセクションではカウンターファクトリーのユニットテストスイートshould_install_and_use_factory_patternと呼ばれる特定のテストに注目します。その名前が示すように、このテストはファクトリーパターンを使用するコントラクトをインストールし、その動作をチェックします。

120行目にて、テストは、カウンタ名new-counter-1と値1を用いて、contract_factoryエントリポイントを呼び出すリクエストのビルドを開始します。134行目では、テストはカウンタ名new-counter-2を用いてcontract_factory_defaultという別のファクトリエントリポイントを呼び出します。デフォルトのカウンター値は0である。

リクエストが処理されると、テストは作成されたコントラクトハッシュをチェックします:

  • 146行目のファクトリーコントラクト
  • 157行目の最初のカウンター
  • 行168の2番目のカウンター

テストは、生成された各カウンターコントラクトのWasmを取得し、各カウンターコントラクトのincrementdecrementのエントリポイントであるWasmエクスポートのテストを進めます。

209行目setup関数は、ファクトリーコントラクトをブロックチェーン上にインストールし、コントラクトファクトリーのハッシュを取得するためのヘルパー関数です。

このファイルに記載されている他テストも必見です:

  • should_not_call_undefined_entrypoints_on_factory – このテストは、テンプレートとしてマークされたエントリーポイントは、ファクトリーコントラクトから直接呼び出すことができないことを確認します。

What’s Next?