ブロックチェーンへの書き込み

ブロックチェーンは、汎用的で公開されており、検証可能なデータベースと考えることができます。ブロックチェーンに書き込むには、一般的なデータベースと同様にトランザクションを作成する必要があります。

通常、ブロックチェーンのトランザクション作成は非常に複雑ですが、Embedded Walletがその複雑さを解消し、5種類のTransactionsを提供しています。

トランザクションの送信は非同期タスクです。SequenceWallet.SendTransactionを呼び出す際にawaitを使えば、TransactionReturnオブジェクトを直接取得できます。もしくは、推奨される方法として、SequenceWallet.OnSendTransactionCompleteSequenceWallet.OnSendTransactionFailedイベント用のハンドラ関数を設定し、どこからでも(awaitなしで)SequenceWallet.SendTransactionメソッドを呼び出すことも可能です。例:

public void OnSendTransactionCompleteHandler(SuccessfulTransactionReturn result) {
    // Do something
}

public void OnSendTransactionFailedHandler(FailedTransactionReturn result) {
    // Do something
}

public void OnWalletCreatedHander(SequenceWallet wallet) {
    wallet.OnSendTransactionComplete += OnSendTransactionCompleteHandler;
    wallet.OnSendTransactionFailed += OnSendTransactionFailedHandler;
}

Unityでイベントの扱いに慣れていない場合は、この素晴らしい Reddit投稿 をご覧ください。

RawTransaction

Transactionの最も基本的な形であるRawTransactionは、ETHや利用中ネットワークのガス通貨をAddressに送る際に非常に便利です。

例えば、1 MATICを0x9766bf76b2E3e7BCB8c61410A3fC873f1e89b43fに送る場合、以下のスニペットを利用できます。

_wallet.SendTransaction(
    Chain.Polygon,
    new Sequence.EmbeddedWallet.Transaction[]
    {
        new RawTransaction("0x9766bf76b2E3e7BCB8c61410A3fC873f1e89b43f", DecimalNormalizer.Normalize(1))
    });

ここで _wallet は SequenceWallet です。

注意:EVMは浮動小数点数をサポートしていません。そのため、トークン(およびガス通貨)の値は整数と「decimals」値で表現されます。1 ETH(または上記例の1 MATIC)は1000000000000000000(1 * 10^18)として表され、ETHやMATICなど多くのガス通貨は「decimals」値が18です。DecimalNormalizer.Normalize(上記)は、input value * 10^decimalsを返す基本的なヘルパー関数で、オプションで「decimals」値を第2引数として指定できます(省略時は18)。

さらに、RawTransactionには16進数形式のデータを文字列として含めることも可能です。詳細は本ドキュメントの上級セクションをご覧ください。

sendERC20

ERC20トークンは、代替可能トークンの標準です。Builderを使えば、ERC20コントラクトのデプロイやトークンのミントも簡単に行えます。詳しくはBuilderのドキュメントをご覧ください。

ERC20トークンのトランザクションを送信するには、次のコードスニペットを利用できます。

_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new SendERC20(
            erc20TokenAddress,
            ToAddress,
            AmountAsString),
    });

注意:上記と同様に、DecimalNormalizer.Normalizeを使って金額を人間が読みやすい形式からEVM形式に変換することを推奨します。ERC20トークンの「decimals」値が18でない場合は、オプションの「decimals」intパラメータを必ず指定してください。ERC20の「decimals」値が分からない場合は、Builderの「Read Contract」内の「decimals」メソッドで簡単に確認できます。

複雑なERC20操作

基本的なトークン送信以外のERC20トークン操作には、SDKに含まれるSequenceEthereumライブラリをご利用ください。Embedded WalletsでRawTransactionsを作成・送信できるよう、ERC20スマートコントラクトのラッパー関数をご用意しています。

まず、コントラクトアドレスと、必要に応じてABI文字列(ERC20標準のカスタムバリエーションを使う場合のみ、推奨はしません)を指定してERC20オブジェクトを作成します。

ERC20 myToken = new ERC20(myTokenAddress);

この参照を使えば、ERC20クラスで実装されているすべてのメソッドにアクセスできます。MintなどCallContractFunctionを返すメソッドは、Embedded WalletsでRawTransactionを作成する際に利用できます。例:

ERC20 myToken = new ERC20(myTokenAddress);
_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new RawTransaction(myToken.Mint(toAddress, DecimalNormalizer.NormalizeAsBigInteger(amount))),
    });

sendERC721

ERC721トークンは非代替性トークン(NFT)の標準です。NFTとして知られています。Builderを使えば、ERC721コントラクトのデプロイやトークンのミントも簡単に行えます。詳しくはBuilderのドキュメントをご覧ください。

ERC721トークンのトランザクションを送信するには、次のコードスニペットを利用できます。

_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new SendERC721(
            erc721TokenAddress,
            ToAddress,
            TokenIdAsString),
    });

複雑なERC721操作

基本的なトークン送信以外のERC721トークン操作には、SDKに付属しているSequenceEthereumライブラリをご利用ください。Embedded WalletsでRawTransactionsを作成・送信できるよう、ERC721スマートコントラクトのラッパー関数をご用意しています。

まず、コントラクトアドレスと、カスタムのERC721規格(推奨されません)を使う場合はオプションでABI文字列を指定して、ERC721オブジェクトを作成します。

ERC721 myToken = new ERC721(myTokenAddress);

この参照を使うことで、ERC721クラスで実装されているすべてのメソッドにアクセスできます。たとえばSafeMintのようにCallContractFunctionを返すメソッドは、Embedded WalletsでRawTransactionを作成する際に利用できます。例:

ERC721 myToken = new ERC721(myTokenAddress);
_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new RawTransaction(myToken.SafeMint(toAddress)),
    });

sendERC1155

ERC1155トークンはマルチトークン規格で、SFT(セミ・ファンジブルトークン)とも呼ばれます。ERC1155規格の共同開発者として、私たちはこの規格がゲームにおいて非常に有用であると確信しています。Builderを使えば、ERC1155コントラクトのデプロイやトークンのミントも簡単です。詳しくはBuilderのドキュメントをご覧ください。

ERC1155トークンのトランザクションを送信するには、次のコードスニペットを利用できます。

_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new SendERC1155(
            erc1155TokenAddress,
            ToAddress,
            new SendERC1155Values[]
            {
                new SendERC1155Values(TokenIdAsString, AmountAsString),
                ...
            }),
    });

注意:同じERC1155コントラクトから複数のトークンIDを一度に送信したい場合は、トランザクション内に複数のSendERC1155Valuesオブジェクトを含めてください。

複雑なERC1155操作

基本的なトークン送信以外のERC1155トークン操作には、SDKに付属しているSequenceEthereumライブラリをご利用ください。Embedded WalletsでRawTransactionsを作成・送信できるよう、ERC1155スマートコントラクトのラッパー関数をご用意しています。

まず、コントラクトアドレスと、カスタムのERC1155規格(推奨されません)を使う場合はオプションでABI文字列を指定して、ERC1155オブジェクトを作成します。

ERC1155 myToken = new ERC1155(myTokenAddress);

この参照を使うことで、ERC1155クラスで実装されているすべてのメソッドにアクセスできます。たとえばMintのようにCallContractFunctionを返すメソッドは、Embedded WalletsでRawTransactionを作成する際に利用できます。例:

ERC1155 myToken = new ERC1155(myTokenAddress);
_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new RawTransaction(myToken.Mint(toAddress, tokenId, amount)),
    });

SequenceContractCall

EVMベースのネットワークでスマートコントラクトを呼び出す際、クライアントは「ABIエンコーディング」と呼ばれる複雑な処理を行い、呼び出したい関数のシグネチャやパラメータをバイナリ形式に変換します。この処理は複雑でミスが起きやすいため、私たちが抽象化し、ユーザーが直接扱う必要がないようにしています。仕組みに興味がある方は、こちらのドキュメントをご覧ください。

SequenceContractCallトランザクションを使うことで、任意のスマートコントラクトの任意のメソッドを呼び出すことができ、複雑なABIエンコーディング処理もサーバー側で対応します。

SequenceContractCallトランザクションを送信するには、次のコードスニペットを利用できます:

_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new SequenceContractCall(ContractAddress, new AbiData(
            FunctionABIAsString,
            ParametersAsObjectArray), ValueAsString),
    });

上記の内容を詳しく見て、分かりにくい変数について理解を深めましょう。

ValueAsString:通常は “0” ですが、payableメソッド(スマートコントラクト定義で payable キーワードが付いているもの)を呼び出す場合は異なります。payableメソッドを利用する場合は、DecimalNormalizer.Normalize を使って、人間が読みやすい形式からEVM形式に金額を変換することを推奨します。なお、ユーザーは指定した金額を支払うための十分な資金をウォレットに用意しておく必要があります。このパラメータは省略可能で、省略時は “0” になります。

FunctionABIAsString:操作したい関数です。コントラクトのソースコード(Etherscanやご利用のネットワークのブロックエクスプローラー)から関数シグネチャ(パラメータ付き)をコピー&ペーストし、空白や変数名を削除して使うことをおすすめします。

ParametersAsObjectArray:呼び出したいメソッドに渡すパラメータです。パラメータ名は不要で、ABIに記載されている順番通りに値だけを指定してください。迷った場合は文字列形式で渡すと良いでしょう。

これらを組み合わせると、SequenceContractCall を使ってERC20の “mint” 関数を呼び出す例は次のようになります:

_wallet.SendTransaction(Chain.Polygon, new Sequence.EmbeddedWallet.Transaction[]
    {
        new SequenceContractCall(ContractAddress, new AbiData(
            "mint(address,uint256)",
            new object[]
            {
                ToAddress, DecimalNormalizer.Normalize(1)
            })),
    });

バッチトランザクション

Sequenceスマートコントラクトウォレットの機能により、SDKを使って複数のトランザクションを簡単にバッチ処理できます。バッチ処理はガス代の節約に大きく貢献し、複雑なトランザクションもすべて成功またはすべて失敗としてまとめて実行できるため、用途ごとにカスタムスマートコントラクトをデプロイする必要がなくなり、新たな設計の可能性が広がります。

バッチトランザクションの送信はとても簡単です。SendTransaction リクエスト時に、トランザクション配列に複数の種類のトランザクションを含めるだけです。

例えば、各タイプのトランザクションをバッチで送信する場合:

_wallet.SendTransaction(
    Chain.Polygon,
    new Sequence.EmbeddedWallet.Transaction[]
    {
        new RawTransaction(ToAddress, DecimalNormalizer.Normalize(1)),
        new SendERC20(
            erc20TokenAddress,
            ToAddress,
            AmountAsString),
        new RawTransaction(new ERC20(erc20TokenAddress).Burn(DecimalNormalizer.NormalizeAsBigInteger(amount))),
        new SendERC721(
            erc721TokenAddress,
            ToAddress,
            TokenIdAsString),
        new SendERC1155(
            erc1155TokenAddress,
            ToAddress,
            new SendERC1155Values[]
            {
                new SendERC1155Values(TokenIdAsString, AmountAsString),
                ...
            }),
        new SequenceContractCall(ContractAddress, new AbiData(
            FunctionABIAsString,
            ParametersAsObjectArray), ValueAsString),
    });

これらのトランザクションは、Sequenceスマートコントラクトウォレットによってネットワークに送信される前に1つのトランザクションにまとめられるため、受け取るトランザクションレシートも1つだけです。

FeeOptions

デフォルトでは、SDKはBuilder APIクレジットを使って、すべてのEmbedded Walletトランザクションのガス代を自動的にスポンサーします。ただし、特定のケースではユーザー自身にガス代を負担してもらいたい場合もあるでしょう。その場合、ユーザーはWeb3に慣れていて、ウォレットにガス代として使えるトークンや通貨を持っている必要があります。選択したネットワークのガス通貨だけでなく、特定のERC20やERC1155トークンでもガス代の支払いが可能です。

トランザクションスポンサー機能はDeveloperプラン以上でご利用いただけます。ガススポンサーについて詳しくはこちらのドキュメントをご覧ください。プロジェクトの課金プランのアップグレード方法についてはこちらのガイドをご参照ください。

まず、バッチで送信したいトランザクションを組み立てます。その後、FeeOptionsをリクエストしてください。

Transaction[] transactions = new Transaction[]
{
    // Create your transactions here
};
FeeOptionsResponse response = await _wallet.GetFeeOptions(chain, transactions);

FeeOptionsResponse には、各 FeeOptionReturn の価格を一定時間ロックするFeeQuote(文字列)が含まれています。これは後ほどトランザクション送信時に必要となります。SDKは自動的にユーザーのウォレットを確認し、Indexer を使ってユーザーが利用可能なFeeOptionsを調べます。

ここから、ユーザーにどの方法で手数料を支払うか選択してもらうUIを表示できます。

ユーザーが支払い方法を選択したら、選択したFeeOptionとFeeQuote文字列を含めてトランザクションを送信します。

_wallet.SendTransactionWithFeeOptions(chain, transactions, response.FeeOptions[selectionIndex].FeeOption, response.FeeQuote);

Package Manager > Samples からインポートできる Demo Scene では、FeeOptionsのシンプルな使用例を確認できます。ここではUIを用意せず、ユーザーのウォレットで利用可能な最初のFeeOptionを自動的に選択しています。実際のゲームでこの方法を使うことは推奨しませんが、ご自身の統合の参考例としてご活用ください。サンプルコードは以下をご覧ください:

private async Task WaitForFeeOptionsAndSubmitFirstAvailable(Address toAddress, string amount)
{
    Transaction[] transactions = new Transaction[]
    {
        new RawTransaction(toAddress, amount)
    };
    FeeOptionsResponse response = await _wallet.GetFeeOptions(_chain, transactions)
    int options = response.FeeOptions.Length;
    for (int i = 0; i < options; i++)
    {
        if (response.FeeOptions[i].InWallet)
        {
            await _wallet.SendTransactionWithFeeOptions(_chain, transactions, response.FeeOptions[i].FeeOption,
                response.FeeQuote);
            return;
        }
    }
    
    Debug.LogError("The user does not have enough of the valid FeeOptions in their wallet");
}

Transaction Queuers

ブロックチェーンを扱う際は、トランザクションをバッチ処理 してガス代を最小限に抑えることが重要です。そのため、SDKには柔軟に設定・拡張できる TransactionQueuer を用意しています。Unityでトランザクションが多いゲームを開発する際のポイントについては、こちらのガイドもご参照ください。

シーンに TransactionQueuer のMonoBehaviourを追加すると、いくつかの設定変数を指定できます。

  • AutoSubmitTransactions:デフォルトはfalseです。これを有効にすると、ThresholdTimeBetweenTransactionsAddedBeforeSubmittedInSeconds で指定した秒数、新しいトランザクションがキューに追加されなかった場合、自動的にキュー内のトランザクションを送信します。
  • ThresholdTimeBetweenTransactionsAddedBeforeSubmittedInSecondsAutoSubmitTransactions == true の場合、指定した秒数の間に新しいトランザクションが追加されなければ、自動的にキュー内のトランザクションを送信します。
  • MinimumTimeBetweenTransactionSubmissionsInSeconds:キューに入れたトランザクションを送信する際の最小間隔を秒単位で指定します。これにより、コード内で TransactionQueuer.SubmitTransactions() を何度呼び出しても、前回の送信から MinimumTimeBetweenTransactionSubmissionsInSeconds 秒が経過していなければトランザクションは送信されません。注意:オプションの overrideWait ブールフラグを true にして TransactionQueuer.SubmitTransactions(overrideWait: true) を呼び出した場合は、MinimumTimeBetweenTransactionSubmissionsInSeconds の経過に関わらず、キュー内のトランザクションが送信されます。

TransactionQueuer では、いくつかのメソッドが利用できます:

  • セットアップ:他のメソッドを呼び出す前に、必ず TransactionQueuer に対して Setup を実行してください。これにより必要な依存関係が作成・キャッシュされます。
  • Enqueue:トランザクションをキューに追加します。
  • SubmitTransactions(bool overrideWait = false, bool waitForReceipt = true):TransactionQueuer による前回のトランザクション送信から MinimumTimeBetweenTransactionSubmissionsInSeconds が経過していれば、キュー内のトランザクションを送信します。overrideWait = true の場合は、すぐにキュー内のトランザクションを送信します。waitForReceipt = false の場合、WaaS API からレスポンスを受け取った時点で TransactionReturn を返します(注:これはWaaS APIがトランザクションレシートの待機中にタイムアウトした場合のみ関係します。waitForReceipt = true の場合は、レシートが返るまでノードに継続的に問い合わせます)。
  • ToString():標準の ToString() 関数をオーバーライドし、より詳細なログ出力をサポートします。
TransactionQueuer には必ず Setup を呼び出すのを忘れないでください!

現在、SDK では TransactionQueuer クラスの2種類の継承クラスが提供されています。

SequenceWalletTransactionQueuer

SequenceWalletTransactionQueuer を使うことで、ユーザーの Sequence Embedded Wallet 用のトランザクションをキューに追加できます。

SequenceWalletTransactionQueuer では、IQueueableTransaction をキューに追加することが求められます。このインターフェースは QueuedTokenTransaction クラスで実装されています。必要に応じて、他のクラスで IQueueableTransaction インターフェースを実装してご利用いただけます。

PermissionedMinterTransactionQueuer

PermissionedMinterTransactionQueuer は、プレイヤーのEmbedded Walletから署名済みメッセージを受け取った際に、バックエンドサーバーからトランザクションをキューに追加して送信する用途向けです。主に、ミント権限が必要なコントラクト(多くのトークンコントラクト)とやり取りする際に、プレイヤーのウォレットへトークンをミントするのに役立ちます。

PermissionedMinterTransactionQueuer では、ミントする TokenId と Amount、必要に応じて IMinter を指定する PermissionedMintTransaction(基本的なデータ転送オブジェクト)をキューに追加します。IMinter を指定しない場合は、デフォルトで PermissionedMinter クラスが使用されます。PermissionedMinter クラスは多くのケースで便利に使え、次の形式でペイロードを送信します:

ProofPayload: 
{
    "app": "Made with Sequence Unity SDK App",
    "iat": (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds(), // issued at time 
    "exp": (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 300, // expiry time 
    "ogn": "Sequence Unity SDK",
    "payload": {
        "contractAddress": "0xabc123...",
        "tokenId": "11",
        "amount": 5
    }
}

This JSON get stringified and included in the MintingRequestProof:
{
    "Proof": "{\"app\": \"Made with Sequence Unity SDK App\", \"iat\": ...}",
    "SignedProof": "0x123def...", // proof signed by the player's embedded wallet
    "SigningAddress": "0xa1b2c3..." // the player's embedded wallet address
}

このペイロードをサーバー側で検証し、ユーザーのアドレスにトークンをミントできます。実装例やセットアップについては、Jelly Forestガイドの該当セクションをご覧ください。

その他の用途では、独自の IMinter クラスを実装することも可能です。これにより、サーバーに送信するペイロードの形式や内容を必要に応じてカスタマイズできます。