モッピー!ポイ活応援ポイントサイト
未分類

イントロダクション

https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/

Solanaでのプログラミング-はじめに

最終更新日:2021年11月19日午後8時4分14秒

読む時間: 65.03分

イントロとモチベーション

このガイドは、Solanaでのコーディングの概要を説明することを目的としています。 (新しいウィンドウを開きます)例としてエスクロープログラムを使用したブロックチェーン。一緒にコードを調べ、エスクロープログラムを段階的に構築します。プログラムを試すために使用できるUIも作成しました。さらに、(恥知らずなプラグ)spl-token-uiで遊ぶことができます (新しいウィンドウを開きます)

このブログ投稿のほとんどの情報は、ドキュメントまたはサンプルプログラムのどこかにあります。そうは言っても、コーディング理論の大部分を段階的に説明し、実際に適用するガイドは見つかりませんでした。この投稿がこれを達成し、ソラナプログラムの理論と実践を織り交ぜることを願っています。ソラナの予備知識は必要ありません。これはRustチュートリアルではありませんが、Rustドキュメントにリンクします (新しいウィンドウを開きます)新しいコンセプトを紹介するときはいつでも。また、関連するSolanaのドキュメントにリンクしますが、フォローするためにそれらを読む必要はありません。

重要な理論は次のように投稿に振りかけられます:

ソラナでは、スマートコントラクトはプログラムと呼ばれます

そして、各セクションの終わりに次のように要約されています。

理論の要約📚
    • ソラナでは、スマートコントラクトはプログラムと呼ばれます

私はすべてのトピックを説明するとは言いませんが、これが読者がソラナをさらに探求するための確かな出発点になることを願っています。SolanaとRustを初めて使用し、この投稿を途切れることなく終了し、説明されているすべての概念と言及されているリンクをしっかりと理解したままにしておきたい場合は、1日を投稿に割り当てることをお勧めします。

何かが機能しておらず、その理由がわからない場合は、ここで最終的なコードを確認してください (新しいウィンドウを開きます)

間違いを見つけたり、フィードバックを送りたい場合は、discord paulx#9059で私に連絡してください。 (新しいウィンドウを開きます)またはツイッター (新しいウィンドウを開きます)

最終製品

コーディングを始める前に、私たちが構築しているものを理解するために最終製品であるエスクロープログラムを見てみましょう。

エスクローとは何ですか?

エスクロースマートコントラクトは、ブロックチェーンが可能にすることを十分に強調しながら、理解しやすく、コード自体に集中できるため、見て構築するのに適した例です。コンセプトに不慣れな方のために、ここに簡単な説明があります。

アリスがアセットAを持ち、ボブがアセットBを持っていると想像してください。彼らは自分の資産を取引したいと思っていますが、どちらも最初に自分の資産を送りたくありません。結局のところ、相手方が取引の終了を延期せず、両方の資産で逃げ出した場合はどうなるでしょうか。誰も最初に資産を送りたくない場合、デッドロックに達します。

この問題を解決する従来の方法は、ABの両方が信頼するサードパーティCを導入することです。これで、AまたはBが最初に移動し、アセットをCに送信できます。Cはその後、自分の資産を送信するだけにしない、相手を待ちCは両方の資産をリリース。

ブロックチェーンの方法は、信頼できるサードパーティCをブロックチェーン上のコードに置き換えることです。具体的には、信頼できるサードパーティと同じように検証可能に機能するスマートコントラクトです。スマートコントラクトは、信頼できるサードパーティよりも優れています。たとえば、信頼できるサードパーティが取引の相手と共謀していないことを確認できますか?コードを実行する前にコードを確認できるため、スマートコントラクトを確認できます。

ここでこの背景セクションを終了します。インターネットには、ブロックチェーンのエスクローに関する多くの資料がすでにあります。それでは、ソラナにそのようなエスクローを構築する方法を見てみましょう。

エスクロープログラムの構築-アリスのトランザクション

プロジェクトの設定

頭の上のテンプレートレポ (新しいウィンドウを開きます)、をクリックUse this templateして、リポジトリを設定します。ソラナのエコシステムはまだ若いので、これが今のところ私たちが持っているものです。Rust拡張機能を備えたVscodeが私が使用しているものです。あなたも必要になりますRust (新しいウィンドウを開きます)。さらに、ここに行きます (新しいウィンドウを開きます)Solana開発ツールをインストールします。(Macを使用していて、必要なバージョンのバイナリがない場合は、「ソースからビルド」セクションに従って、インストールされているビンをパスに追加します。このsolana-install init手順は不要であり、機能しません。無視してください。コマンドが見つからないためビルドします。coreutilsをインストールしてみてください (新しいウィンドウを開きます)およびbinutils (新しいウィンドウを開きます)自作で)。

solanaプログラムをテストする方法がまだわからない場合は、すべてのテストコードを削除してください。プログラムのテストは、別のブログ投稿のトピックです。の横lib.rstestsフォルダだけでなく、のテストコードも削除しますsrc。最後に、テストの依存関係をから削除しますCargo.toml (新しいウィンドウを開きます)。これで、次のようになります。

[features]
test-bpf = []

[dev-dependencies]
assert_matches = “1.4.0”
solana-program-test = “=1.9.1”
solana-sdk = “=1.9.1”
solana-validator = “=1.9.1”

これを削除した。

[package]
name = "solana-escrow"
version = "0.1.0"
edition = "2018"
license = "WTFPL"
publish = false

[dependencies]
solana-program = "1.6.9"

[lib]
crate-type = ["cdylib", "lib"]
1
2
3
4
5
6
7
8
9
10
11
12

entrypoint.rs、プログラム、およびアカウント

を見てくださいlib.rs。まず、必要な木枠 (新しいウィンドウを開きます)使用を使用してスコープに持ち込まれます (新しいウィンドウを開きます)。次に、マクロを使用しますentrypoint!  (新しいウィンドウを開きます)process_instruction関数をエントリポイントとして宣言します (新しいウィンドウを開きます)プログラムに。エントリポイントは、プログラムを呼び出す唯一の方法です。すべての呼び出しは、エントリポイントとして宣言された関数を通過します。

呼び出されると、プログラムはそのBPFローダーに渡されます (新しいウィンドウを開きます)呼び出しを処理します。BPFローダーが異なれば、必要なエントリポイントも異なります。

複数のBPFローダーが存在する理由は、それ自体がプログラムであるためです。プログラムが更新された場合は、新しいバージョンのプログラムを展開する必要があります。使用しているBPFローダーでは、エントリポイント関数が3つの引数を取る必要があることがわかります。program_id単に現在実行中のプログラムのプログラムIDです。プログラム内でアクセスしたい理由は後で明らかになります。intruction_data呼び出し元によってプログラムに渡されるデータであり、何でもかまいません。最後に、何であるかを理解するにaccountsは、solanaプログラミングモデルをさらに深く掘り下げる必要があります (新しいウィンドウを開きます)。アカウントが必要な理由は

Solanaプログラムはステートレスです

状態を保存する場合は、アカウントを使用します (新しいウィンドウを開きます)。プログラム自体は、のマークが付いたアカウントに保存されますexecutable。各アカウントはデータとSOLを保持できます (新しいウィンドウを開きます)。各アカウントにもがありowner、所有者のみがアカウントから借方に記入してデータを調整できます。クレジットは誰でも行うことができます。これがアカウントの例です (新しいウィンドウを開きます)。サンプルアカウントでわかるように、所有者フィールドがに設定されていSystem Programます。実際のところ、

アカウントはプログラムによってのみ所有できます

今、あなたは「それは私自身のSOLアカウントが実際に私自身によって所有されていないことを意味するのか?」と考えているかもしれません。そして、あなたは正しいでしょう!しかし、恐れることはありません、あなたの資金はサフです (新しいウィンドウを開きます)。それが機能する方法は、基本的なSOLトランザクションでさえSolanaのプログラムによって処理されるということsystem programです。(実際のところ、プログラムもプログラムによって所有されています。アカウントに保存され、これらのexecutableアカウントはBPFローダーによって所有されていることを忘れないでください。BPFローダーによって所有されていないプログラムは、もちろん、BPFローダー自体とシステムプログラム。これらはNativeLoaderによって所有され、メモリの割り当てやアカウントの実行可能としてのマーク付けなどの特別な特権を持っています)

システムプログラムを見れば (新しいウィンドウを開きます)プログラムはすべての基本的なSOLアカウントを所有していますが、借方に記入されているSOLアカウントの秘密鍵によってトランザクションが署名されている場合にのみ、アカウントからSOLを転送できることがわかります。

理論的には、プログラムは所有するアカウントに対して完全な自律性を持っています。この自律性を制限するのはプログラムの作成者の責任であり、プログラムの作成者が実際にそうしていることを確認するのはプログラムのユーザーの責任です。

プログラムがトランザクションに署名されているかどうかを確認する方法と、プログラムがアカウントの所有者になる方法について少し説明します。エントリポイントのセクションを締めくくる前に、もう1つ知っておくべきことがあります。

読み取りまたは書き込みが行われるすべてのアカウントは、エントリポイント関数に渡される必要があります

これにより、ランタイムはトランザクションを並列化できます。ランタイムは、すべてのユーザーに常に書き込まれ、読み取られるすべてのアカウントを知っている場合、同じアカウントにアクセスしたり、同じアカウントにアクセスしたりせず、読み取りと書き込みを行わないトランザクションを並行して実行できます。トランザクションがこの制約に違反し、ランタイムが通知されていないアカウントに対して読み取りまたは書き込みを行うと、トランザクションは失敗します。

最後にこのセクションを終了するには、entrypoint.rs横に新しいファイルを作成し、そこにコードlib.rsを移動しlib.rsます。最後に、エントリポイントモジュールを内に登録しますlib.rs作成するすべてのファイルに対してこれを行う必要があります

// inside lib.rs, only the following line should be in here
pub mod entrypoint;
1
2
理論の要約📚
    • 各プログラムはそのBPFローダーによって処理され、使用されるBPFローダーに依存する構造のエントリポイントがあります。
    • アカウントは状態を保存するために使用されます
    • アカウントはプログラムによって所有されています
    • アカウントの所有者のみがアカウントから借方に記入し、そのデータを調整できます
    • 書き込みまたは読み取りを行うすべてのアカウントは、エントリポイントに渡される必要があります

Instruction.rsパート1、一般的なコード構造、およびエスクロープログラムフローの開始

コード構造

次に、instruction.rs他の2つのファイルの隣にファイルを作成lib.rsし、エントリポイントで行ったように内部に登録します。新しいファイルの目的を理解するために、プログラムのコードを構造化する一般的な方法と、プログラムを構造化する方法を見てみましょう。

.
├─ src
│  ├─ lib.rs -> registering modules
│  ├─ entrypoint.rs -> entrypoint to the program
│  ├─ instruction.rs -> program API, (de)serializing instruction data
│  ├─ processor.rs -> program logic
│  ├─ state.rs -> program objects, (de)serializing state
│  ├─ error.rs -> program specific errors
├─ .gitignore
├─ Cargo.lock
├─ Cargo.toml
├─ Xargo.toml
1
2
3
4
5
6
7
8
9
10
11
12

この構造を使用したプログラムのフローは次のようになります。

  1. 誰かがエントリポイントを呼び出します
  2. エントリポイントは引数をプロセッサに転送します
  3. プロセッサは、エントリポイント関数から引数instruction.rsをデコードするように要求しinstruction_dataます。
  4. デコードされたデータを使用して、プロセッサは要求の処理に使用する処理関数を決定します。
  5. プロセッサはstate.rs、エントリポイントに渡されたアカウントの状態をエンコードまたはデコードするために使用できます。

ご覧のように、

Instruction.rsは、プログラムの「API」を定義します

エントリポイントは1つだけですが、プログラムの実行は、内部でデコードされる特定の命令データに応じて異なるパスをたどることができますinstruction.rs

エスクロープログラムフローの開始

次に、エスクロープログラムのプログラムフローをズームアウトしてスケッチすることにより、プログラムがたどる可能性のあるさまざまな実行パスを見てみましょう。

アリスボブの2つのパーティがあることを思い出してくださいsystem_program。つまり、2つのアカウントがあります。アリスボブはトークンを転送したいので、私たちはそれを利用します-あなたはそれを推測しました!- token program。トークンプログラムでは、トークンを保持するには、トークンアカウントが必要です。アリスボブの両方がトークンごとにアカウントを必要とします。したがって、さらに4つのアカウントを取得します。トークンをXとトークンYと呼びます。アリスは、XとYのアカウントを取得し、ボブも同様です。(私たち自身のトークンはXとYですが、このエスクローはUSDCなどのソラナのすべてのトークンで機能します (新しいウィンドウを開きます)およびSRM (新しいウィンドウを開きます))。エスクローの作成と取引は単一のトランザクション内では発生しないため、エスクローデータを保存するために別のアカウントを用意することをお勧めします(たとえば、トークンYのアリスがトークンXと引き換えに必要な量を保存しますが、後でそれについて説明します!)。このアカウントは取引所ごとに作成されることに注意してください。今のところ、私たちの世界は次のようになっています。

今、あなたがあなた自身に尋ねるかもしれない2つの質問があります。アリスとボブはそれぞれXとYの所有権をエスクローに譲渡し、メインアカウントはトークンアカウントにどのように接続されますか?これらの質問に対する答えを見つけるには、簡単ににジャンプする必要がありtoken programます。

トークンプログラムパート1

トークンの所有権

アリスのメインアカウントをトークンアカウントに接続する素朴な方法は、それらをまったく接続しないことです。彼女がトークンを転送したいときはいつでも、彼女はトークンアカウントの秘密鍵を使用していました。明らかに、アリスが多くのトークンを所有している場合、トークンアカウントごとに秘密鍵を保持する必要があるため、これは持続可能ではありません。

アリスがすべてのトークンアカウントに対して1つの秘密鍵を持っていれば、それははるかに簡単です。これはまさにトークンプログラムが行う方法です。各トークンアカウントに所有者を割り当てます。このトークンアカウント所有者属性は、アカウント所有者と同じではないことに注意してください。アカウント所有者は、常にプログラムとなる内部Solana属性です。新しいトークン所有者属性は、トークンプログラムがユーザースペース(つまり、ビルドしているプログラム)で宣言するものです。他のプロパティdataに加えて、トークンアカウント内にエンコードされます (新しいウィンドウを開きます)アカウントが保持しているトークンの残高など。これが意味することは、トークンアカウントが設定されると、その秘密鍵は役に立たず、トークン所有者属性のみが重要になるということです。また、トークン所有者属性は別のアドレスになります。この場合は、それぞれアリスとボブのメインアカウントです。トークン転送を行うときは、メインアカウントの秘密鍵を使用してtx(tx = transaction)に署名するだけです。

Solanaの内部アカウント情報はすべて、アカウントのフィールドに保存されます (新しいウィンドウを開きます)しかしdata、ユーザースペース情報のみを目的としたフィールドには決して入りません

エクスプローラーでトークンアカウントを見ると、これらすべてを見ることができます (新しいウィンドウを開きます)。アカウントのdataプロパティを解析し、適切にフォーマットされたユーザースペースフィールドを表示します。

mintエクスプローラーのフィールドに気づいたかもしれません。これにより、トークンアカウントがどのトークンに属しているかを知ることができます。トークンごとに、サプライなどのトークンのメタデータを保持する1つのミントアカウントがあります。アリスとボブが使用するトークンアカウントが実際にアセットXとYに属していること、およびどちらの当事者も間違ったアセットに忍び込んでいないことを確認するために、後でこのフィールドが必要になります。

このすべてを念頭に置いて、私たちは私たちの世界にもっと多くの情報を投入することができます:

簡単にするために、プログラムとその矢印を削除し、他のアカウントに適切な色を付けました。繰り返しになりますが、図の「所有者」の矢印は、内部のsolanaアカウントの所有権ではなく、ユーザースペースの所有権を示しています。

これで、これらすべてのアカウントがどのように接続されているかはわかりましたが、アリスがどのようにトークンをエスクローに転送できるかはまだわかりません。これについては今から説明します。

所有権の譲渡

トークンのユニットを所有する唯一の方法は、アカウントの(ユーザースペース)mintプロパティによって参照されるトークンのトークンバランスを保持するトークンアカウントを所有することです。したがって、エスクロープログラムには、アリスのXトークンを保持するためのアカウントが必要になります。これを実現する1つの方法は、アリスに一時的なXトークンアカウントを作成させ、取引したいXトークンを転送することです(トークンプログラムは、同じミントのトークンアカウントの所有者になる可能性のあるトークンアカウントの数に制限を設定しません)。次に、トークンプログラムの関数を使用して、一時的なXトークンアカウントの所有権(トークンプログラム)をエスクロープログラムに譲渡します。一時的なアカウントをエスクローの世界に追加しましょう。この画像は、アリスがトークンアカウントの所有権を譲渡する前のエスクローの世界を示しています。

ここにもう1つ問題があります。アリスは正確に何に所有権を譲渡しますか?プログラムから派生したアドレスを入力します (新しいウィンドウを開きます)

理論の要約📚

プログラム派生アドレス(PDA)パート1

エスクローが開いていてボブのトランザクションを待っている間に、プログラムがXトークンを所有する方法が必要です。問題は、プログラムにトークンアカウントのユーザースペースの所有権を与えることができるかどうかです。

秘訣は、トークンアカウントの所有権をエスクロープログラムのプログラム派生アドレス(PDA)に割り当てることです。今のところ、このアドレスが存在することを知っていれば十分であり、プログラムにトランザクションに署名させたり、アカウントのユーザースペース所有権を割り当てたりするために使用できます。PDAについては後で詳しく説明しますが、とりあえずコーディングに戻りましょう。

Instruction.rsパート2

instruction.rsこのファイルがプログラムのAPIを定義することを知っていましたが、まだコードを記述していませんでした。InitEscrowAPIエンドポイントを追加してコーディングを始めましょう。

// inside instruction.rs
pub enum EscrowInstruction {

    /// Starts the trade by creating and populating an escrow account and transferring ownership of the given temp token account to the PDA
    ///
    ///
    /// Accounts expected:
    ///
    /// 0. `[signer]` The account of the person initializing the escrow
    /// 1. `[writable]` Temporary token account that should be created prior to this instruction and owned by the initializer
    /// 2. `[]` The initializer's token account for the token they will receive should the trade go through
    /// 3. `[writable]` The escrow account, it will hold all necessary info about the trade.
    /// 4. `[]` The rent sysvar
    /// 5. `[]` The token program
    InitEscrow {
        /// The amount party A expects to receive of token Y
        amount: u64
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

instruction.rsアカウントに触れていない、あなたがそうここに必要なすべての呼び出しの情報を期待してアカウントを定義すると便利である一つの場所や他人のために見つけるのは簡単です。さらに、必要なアカウントプロパティを角かっこで囲んで追加すると便利です。///コメントの後のすべてはプログラムに影響を与えないことに注意してください。それは文書化の目的でのみ存在します。writableプロパティは、私が上記で説明並列化を思い出させる必要があります。呼び出し元が呼び出し元のwritableコードでアカウントにマークを付けていないのに、プログラムがアカウントに書き込もうとすると、トランザクションは失敗します。

エンドポイントがそのように見える理由を説明しましょう。

0. `[signer]` The account of the person initializing the escrow
1

一時アカウントの所有権を譲渡するにはアリスの署名が必要なため、署名者としてアカウント0、具体的にはアカウント0が必要です。コードでは、アリスをイニシャライザー、ボブをテイカーと呼びます(アリスはエスクローを開始し、ボブは取引を行います。より良い名前を思い付くことができるかどうか教えてください)

1. `[writable]` Temporary token account that should be created prior to this instruction and owned by the initializer
1

アカウント1は、書き込み可能である必要がある一時トークンXアカウントです。これは、トークンアカウントの所有権の変更はユーザースペースの変更でdataあり、アカウントのフィールドが変更されることを意味するためです。

2. `[]` The initializer's token account for the token they will receive should the trade go through
1

アカウント2はアリスのトークンYアカウントです。最終的には書き込まれますが、このトランザクションでは発生しません。そのため、角かっこを空のままにしておくことができます(読み取り専用を意味します)。

3. `[writable]` The escrow account, it will hold all necessary info about the trade.
1

アカウント3はエスクローアカウントであり、プログラムがエスクロー情報を書き込むため、書き込み可能である必要があります。

4. `[]` The rent sysvar
1

アカウント4はRentsysvarです。processorコードを書き始めたら、これについて詳しく説明します。

5. `[]` The token program
1

今のところ覚えておくべきことは、アカウント5はトークンプログラム自体のアカウントであるということです。processorコードを書くときに、なぜこのアカウントが必要なのかを説明します。

Solanaには、使用しているSolanaクラスターのパラメーターであるsysvarがあります。これらのsysvarは、アカウントを介してアクセスし、現在の料金や家賃などのパラメーターを保存できます。solana-programバージョンの時点で1.6.5sysvarsは、アカウントとしてエントリポイントに渡されることなくアクセスすることもできます (新しいウィンドウを開きます)(このチュートリアルでは、今のところ古い方法を引き続き使用しますが、使用しないでください!)。

InitEscrow {
    /// The amount party A expects to receive of token Y
    amount: u64
}
1
2
3
4

最後に、プログラムは、アリスがXトークンに対して受け取りたいトークンYの量を必要とします。この金額は、アカウントではなく、を通じて提供されますinstruction_data

instruction.rsデコードを担当しているinstruction_dataので、次に行います。

// inside instruction.rs
use std::convert::TryInto;
use solana_program::program_error::ProgramError;

use crate::error::EscrowError::InvalidInstruction;

 pub enum EscrowInstruction {

    /// Starts the trade by creating and populating an escrow account and transferring ownership of the given temp token account to the PDA
    ///
    ///
    /// Accounts expected:
    ///
    /// 0. `[signer]` The account of the person initializing the escrow
    /// 1. `[writable]` Temporary token account that should be created prior to this instruction and owned by the initializer
    /// 2. `[]` The initializer's token account for the token they will receive should the trade go through
    /// 3. `[writable]` The escrow account, it will hold all necessary info about the trade.
    /// 4. `[]` The rent sysvar
    /// 5. `[]` The token program
    InitEscrow {
        /// The amount party A expects to receive of token Y
        amount: u64
    }
}

impl EscrowInstruction {
    /// Unpacks a byte buffer into a [EscrowInstruction](enum.EscrowInstruction.html).
    pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
        let (tag, rest) = input.split_first().ok_or(InvalidInstruction)?;

        Ok(match tag {
            0 => Self::InitEscrow {
                amount: Self::unpack_amount(rest)?,
            },
            _ => return Err(InvalidInstruction.into()),
        })
    }

    fn unpack_amount(input: &[u8]) -> Result<u64, ProgramError> {
        let amount = input
            .get(..8)
            .and_then(|slice| slice.try_into().ok())
            .map(u64::from_le_bytes)
            .ok_or(InvalidInstruction)?;
        Ok(amount)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

unpack参照を期待します (新しいウィンドウを開きます)のスライスにu8。最初のバイト(= tag)を調べて、デコード方法を決定します(match (新しいウィンドウを開きます)restスライスの残り(= )。今のところ、1つの命令のままにしておきます(ボブがトレードを行う命令は無視します)。unpack_amountをデコードしrestて、をu64表すを取得しamountます。個々の機能を自分で調べることができます。今のところ最も重要なことは、アンパック関数で高レベルで何が起こっているかを理解することです。1。ビルドする命令を選択します。2。その命令をビルドして返します。

未定義のエラーを使用しているため、これはコンパイルされません。次にそのエラーを追加しましょう。

理論の要約📚

error.rs

error.rs他のファイルの隣に新しいファイルを作成し、内部に登録しますlib.rs。次に、次の依存関係をCargo.toml

...
[dependencies]
solana-program = "1.6.9"
thiserror = "1.0.24"
1
2
3
4

および次のコードをerror.rs

// inside error.rs
use thiserror::Error;

#[derive(Error, Debug, Copy, Clone)]
pub enum EscrowError {
    /// Invalid instruction
    #[error("Invalid Instruction")]
    InvalidInstruction,
}
1
2
3
4
5
6
7
8
9

ここで行っているのは、エラータイプの定義です (新しいウィンドウを開きます)fmt::Displayリンクが指す例で行われているように自分で実装を作成する代わりに、便利なthiserrorを使用します (新しいウィンドウを開きます)を使用して私たちのためにそれを行うライブラリ #[error("..")] (新しいウィンドウを開きます)表記。これは、後でエラーを追加するときに特に役立ちます。

振り返ってみるとinstruction.rs、まだ終わっていないことがわかります。コンパイラは、EscrowErrorをProgramErrorに変換する方法がないと言っています(「トレイトstd::convert::From<error::EscrowError>は実装されていませんsolana_program::program_error::ProgramError」)。それでは、方法を実装しましょう。

// inside error.rs
use thiserror::Error;

use solana_program::program_error::ProgramError;

#[derive(Error, Debug, Copy, Clone)]
pub enum EscrowError {
    /// Invalid instruction
    #[error("Invalid Instruction")]
    InvalidInstruction,
}

impl From<EscrowError> for ProgramError {
    fn from(e: EscrowError) -> Self {
        ProgramError::Custom(e as u32)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

ここで何が起こっているのかを理解するために少し立ち止まりましょう。私たちは実装されている一般的な形質を具体的には、From (新しいウィンドウを開きます)?オペレーターが使用したい特性。この特性を実装fromするには、変換を実行する関数を実装する必要があります。ProgramError列挙型は、提供Custom私たちは私たちのプログラムのから変換することができますバリアントEscrowErrorにしますProgramError

そもそもこの変換を行う理由は、エントリポイントがResult何も返さないか、を返すためですProgramError

Processor.rsパート1、レンタルパート1、InitEscrow命令の処理を開始

パブfnプロセス

エントリポイント、InitEscrowエンドポイント、および最初のエラーを作成した後、最終的にコードに進むことができますprocessor.rs。ここで魔法が起こります。processor.rs内で作成して登録することから始めlib.rsます。次に、以下をに貼り付けますprocessor.rs

use solana_program::{
    account_info::AccountInfo,
    entrypoint::ProgramResult,
    msg,
    pubkey::Pubkey,
};

use crate::instruction::EscrowInstruction;

pub struct Processor;
impl Processor {
    pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult {
        let instruction = EscrowInstruction::unpack(instruction_data)?;

        match instruction {
            EscrowInstruction::InitEscrow { amount } => {
                msg!("Instruction: InitEscrow");
                Self::process_init_escrow(accounts, amount, program_id)
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

何が起こっているのかを開梱しましょう。まず、instruction_datafromentrypoint.rsを保持しているスライスへの参照を、unpack前に作成した関数に渡します(関数呼び出しの後のことに注意してください)? (新しいウィンドウを開きます))。match呼び出す処理関数を把握するために使用します。ささいなことですが、今のところ。msg!私たちが行くところを記録します。

fn process_init_escrow

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    program_error::ProgramError,
    msg,
    pubkey::Pubkey,
};
...
impl Processor {
    pub fn process{...}
    
    fn process_init_escrow(
        accounts: &[AccountInfo],
        amount: u64,
        program_id: &Pubkey,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let initializer = next_account_info(account_info_iter)?;

        if !initializer.is_signer {
            return Err(ProgramError::MissingRequiredSignature);
        }

        Ok(())
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

process_init_escrow次です。明確にするために、...あなたが見ているコードの周りに他のものがあることを意味します、それらをコピーしないでください!現在のものuse solana_program...をコピーしてここにあるものに置き換えるか、個々の不足している部分を追加します。

内部でprocess_init_escrowは、最初にアカウントのイテレータを作成します。要素を取り出すことができるように、変更可能である必要があります。私たちが期待する最初のアカウントは、で定義されてinstruction.rsいるように、エスクローの初期化子、つまりアリスのメインアカウントです。彼女は私たちがすぐにチェックする署名者である必要があります。のブールフィールドAccountInfoです。

...
fn process_init_escrow(
    accounts: &[AccountInfo],
    amount: u64,
    program_id: &Pubkey,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let initializer = next_account_info(account_info_iter)?;

    if !initializer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let temp_token_account = next_account_info(account_info_iter)?;

    let token_to_receive_account = next_account_info(account_info_iter)?;
    if *token_to_receive_account.owner != spl_token::id() {
        return Err(ProgramError::IncorrectProgramId);
    }

    Ok(())
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

次に、強調表示された行を追加します。一時トークンアカウントは書き込み可能である必要がありますが、これを明示的にチェックする必要はありません。アリスがアカウントを書き込み可能としてマークしない場合、トランザクションは自動的に失敗します。

token_to_receive_accountトークンプログラムが実際に所有していることを確認するのに、なぜ同じことをしないのか」と自問するかもしれませんtemp_token_account。答えは後で機能で、我々はの転送所有権にトークンプログラムを依頼することであるtemp_token_accountPDAtemp_token_accountがトークンプログラムによって所有されていない場合、この転送は失敗します。これは、ご存知のとおり、アカウントを所有するプログラムのみがアカウントを変更できるためです。したがって、ここに別のチェックを追加する必要はありません。

token_to_receive_accountただし、(アリスのトランザクション内で)変更は行いません。ボブが取引を行うときにエスクローが資産Yの送信先を認識できるように、エスクローデータに保存するだけです。したがって、このアカウントには、チェックを追加する必要があります。そうしなければ、ひどいことは何も起こらないことに注意してください。追加されたチェックが原因でアリスのトランザクションが失敗する代わりに、トークンプログラムがYトークンをアリスに送信しようとしますが、の所有者ではないため、ボブは失敗しますtoken_to_receive_account。とはいえ、実際に無効な状態につながるtxを失敗させる方が合理的と思われます。

最後に、ここではCargo.tomlまだ登録されていないクレートを使用していることに気付いたと思います。今それをしましょう。

[dependencies]
solana-program = "1.6.9"
thiserror = "1.0.24"
spl-token = {version = "3.1.1", features = ["no-entrypoint"]}
1
2
3
4

ここでは、他の依存関係とは少し異なる方法で依存関係をインポートしています。これは、独自のエントリポイントを持つ別のSolanaプログラムをインポートしているためです。ただし、プログラムには、前に定義したエントリポイントを1つだけ含める必要があります。幸いなことに、トークンプログラムは、貨物機能の助けを借りてエントリポイントをオフにするスイッチを提供します (新しいウィンドウを開きます)。他の人が私たちのプログラムをインポートできるように、私たちのプログラムでもこの機能を定義する必要があります!ヒントをいくつか残しておきます。トークンプログラムをチェックしてください。 (新しいウィンドウを開きます)Cargo.tomlとそのlib.rs。自分で理解できない、または理解したくない場合は、私が作成したエスクロープログラムを調べることができます。

に戻りprocessor.rsます。solana_programuseステートメントをコピーして置き換え、次のコードを追加しますprocess_init_escrow

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    program_error::ProgramError,
    msg,
    pubkey::Pubkey,
    program_pack::{Pack, IsInitialized},
    sysvar::{rent::Rent, Sysvar},
};
//inside process_init_escrow
...
let temp_token_account = next_account_info(account_info_iter)?;

let token_to_receive_account = next_account_info(account_info_iter)?;
if *token_to_receive_account.owner != spl_token::id() {
    return Err(ProgramError::IncorrectProgramId);
}

let escrow_account = next_account_info(account_info_iter)?;
let rent = &Rent::from_account_info(next_account_info(account_info_iter)?)?;

if !rent.is_exempt(escrow_account.lamports(), escrow_account.data_len()) {
    return Err(EscrowError::NotRentExempt.into());
}

let mut escrow_info = Escrow::unpack_unchecked(&escrow_account.try_borrow_data()?)?;
if escrow_info.is_initialized() {
    return Err(ProgramError::AccountAlreadyInitialized);
}

Ok(())
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

今、私たちは Rent (新しいウィンドウを開きます)動作中のsysvar。説明させてください:

家賃は、定期的に必要なスペース(つまり、アカウントとそのフィールドがメモリ内で占めるスペース)に応じてアカウントの残高から差し引かれます。ただし、アカウントの残高が、消費しているスペースに応じたしきい値よりも高い場合は、アカウントをレント免除にすることができます。

ほとんどの場合、あなたはあなたの口座を家賃免除にしたいのです。なぜなら、それらの残高がゼロになると、それらは消えしまうからです。これについては、ボブのトランザクションの最後に詳しく説明します。

また、新しいエラーバリアントを内部に追加しerror.rs、useステートメントを調整してください。

から

use crate::instruction::EscrowInstruction;
1

use crate::{instruction::EscrowInstruction, error::EscrowError};
1

もう一つのなじみのないことがここで起こっています。初めて、dataフィールドにアクセスします。のでdataまたの単なる配列でu8、我々はとそれをデシリアライズする必要がありますEscrow::unpack_unchecked。これはstate.rs、次のセクションで作成する関数です。

理論の要約📚
    • 家賃は、定期的に必要なスペースに応じてアカウントの残高から差し引かれます。ただし、アカウントの残高が、消費しているスペースに応じたしきい値よりも高い場合は、アカウントをレント免除にすることができます。

state.rs

内に作成state.rsして登録しますlib.rs。状態ファイルは、1)プロセッサが使用できる状態オブジェクトを定義する役割を果たします。2)そのようなオブジェクトをu8それぞれの配列との間でシリアル化および逆シリアル化します。

に以下を追加することから始めますstate.rs

use solana_program::pubkey::Pubkey;

pub struct Escrow {
    pub is_initialized: bool,
    pub initializer_pubkey: Pubkey,
    pub temp_token_account_pubkey: Pubkey,
    pub initializer_token_to_receive_account_pubkey: Pubkey,
    pub expected_amount: u64,
}
1
2
3
4
5
6
7
8
9

temp_token_account_pubkeyボブが取引を行うときに、エスクロープログラムがtemp_token_account_pubkeyのアカウントからボブのアカウントにトークンを送信できるように保存する必要があります。ボブが最終的にアカウントをエントリポイント呼び出しに渡す必要があることはすでにわかっていますが、なぜここに保存するのでしょうか。まず、公開鍵をここに保存すると、ボブはエントリポイントに渡す必要のあるアカウントのアドレスを簡単に見つけることができます。そうでなければ、アリスは彼にエスクロー口座の住所だけでなく、彼女のすべての口座の住所も送る必要があります。第二に、セキュリティにとってより重要なのは、ボブが別のトークンアカウントを渡すことができるということです。temp_token_account_pubkey公開鍵としてアカウントを渡すことを要求する小切手を追加しなければ、彼がそうすることを妨げるものは何もありません。そして、後でプロセッサにそのチェックを追加するには、を保存するためのInitEscrow命令が必要temp_token_account_pubkeyです。

Solanaプログラムを作成するときは、内部のAPIで定義されているものとは異なるアカウントを含め、任意のアカウントがエントリポイントに渡される可能性があることに注意してinstruction.rsください。それをチェックするのはプログラムの責任ですreceived accounts == expected accounts

initializer_token_to_receive_account_pubkeyボブが取引を行うときに、彼のトークンをそのアカウントに送信できるように、保存する必要があります。expected_amountボブが十分なトークンを送信したことを確認するために使用されます。その葉initializer_pubkeyis_initialized。後者については今説明し、前者については後で説明します。

is_initialized特定のエスクローアカウントがすでに使用されているかどうかを判断するために使用します。これ、シリアル化、および逆シリアル化はすべて、特性で標準化されています (新しいウィンドウを開きます)program packモジュール (新しいウィンドウを開きます)。まず、とを実装SealedIsInitializedます。

// inside state.rs
use solana_program::{
    program_pack::{IsInitialized, Pack, Sealed},
    pubkey::Pubkey,
};
...
impl Sealed for Escrow {}

impl IsInitialized for Escrow {
    fn is_initialized(&self) -> bool {
        self.is_initialized
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

SealedSized両者の間に違いはないようですが、これはソラナのバージョンのルストの特徴です。さて、Packこれは実装に依存してSealedおり、この場合もIsInitialized実装に依存しています。これは大きくて単純なコードブロックです。2つに分けます。最初のものから始めましょう(useインポートを再度コピーして置き換えることができます):

// inside state.rs
use solana_program::{
    program_pack::{IsInitialized, Pack, Sealed},
    program_error::ProgramError,
    pubkey::Pubkey,
};

use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
...
impl Pack for Escrow {
    const LEN: usize = 105;
    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
        let src = array_ref![src, 0, Escrow::LEN];
        let (
            is_initialized,
            initializer_pubkey,
            temp_token_account_pubkey,
            initializer_token_to_receive_account_pubkey,
            expected_amount,
        ) = array_refs![src, 1, 32, 32, 32, 8];
        let is_initialized = match is_initialized {
            [0] => false,
            [1] => true,
            _ => return Err(ProgramError::InvalidAccountData),
        };

        Ok(Escrow {
            is_initialized,
            initializer_pubkey: Pubkey::new_from_array(*initializer_pubkey),
            temp_token_account_pubkey: Pubkey::new_from_array(*temp_token_account_pubkey),
            initializer_token_to_receive_account_pubkey: Pubkey::new_from_array(*initializer_token_to_receive_account_pubkey),
            expected_amount: u64::from_le_bytes(*expected_amount),
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

何かを実装Packするための最初の要件LENは、タイプのサイズを定義することです。エスクロー構造体を見ると、個々のデータ型のサイズを追加することにより、構造体の長さを計算する方法がわかります1 (bool) + 3 * 32 (Pubkey) + 1 * 8 (u64) = 105u8コーディングが簡単になり、余分な無駄なビットのコストがごくわずかであるため、ブール値に全体を使用しても問題ありません。

エスクローの長さを定義した後unpack_from_slice、の配列をu8上記で定義したエスクロー構造体のインスタンスに変換する実装を行います。ここではあまり興味深いことは起こりません。ここで注目すべきは、arrayrefの使用です。 (新しいウィンドウを開きます)、スライスのセクションへの参照を取得するためのライブラリ。ドキュメントは、ライブラリの(わずか4つの)異なる関数を理解するのに十分なはずです。必ずライブラリをに追加してくださいCargo.toml

...
[dependencies]
...
arrayref = "0.3.6"
...
1
2
3
4
5

これで状態を逆シリアル化できます。次はシリアル化です。

...
impl Pack for Escrow {
    ...
    fn pack_into_slice(&self, dst: &mut [u8]) {
        let dst = array_mut_ref![dst, 0, Escrow::LEN];
        let (
            is_initialized_dst,
            initializer_pubkey_dst,
            temp_token_account_pubkey_dst,
            initializer_token_to_receive_account_pubkey_dst,
            expected_amount_dst,
        ) = mut_array_refs![dst, 1, 32, 32, 32, 8];

        let Escrow {
            is_initialized,
            initializer_pubkey,
            temp_token_account_pubkey,
            initializer_token_to_receive_account_pubkey,
            expected_amount,
        } = self;

        is_initialized_dst[0] = *is_initialized as u8;
        initializer_pubkey_dst.copy_from_slice(initializer_pubkey.as_ref());
        temp_token_account_pubkey_dst.copy_from_slice(temp_token_account_pubkey.as_ref());
        initializer_token_to_receive_account_pubkey_dst.copy_from_slice(initializer_token_to_receive_account_pubkey.as_ref());
        *expected_amount_dst = expected_amount.to_le_bytes();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

これはunpack_from_slice関数とほとんど同じですが、その逆も同様です。今回は、も渡し&selfます。まだ自己がなかったunpack_from_sliceので、私たちは内部でこれをする必要はありませんでした。unpack_from_sliceエスクロー構造体の新しいインスタンスを返す静的コンストラクター関数でした。の場合pack_into_slice、すでにEscrow構造体のインスタンスがあり、指定されたdstスライスにシリアル化します。以上ですstate.rs。しかし、待ってください。振り返ってみるprocessor.rsunpack_unchecked、定義していない関数を呼び出します。それはどこから来ているのでしょうか。答えは、トレイトにはオーバーライドできるデフォルトの関数を含めることができますが、オーバーライドする必要はないということです。 ここを見て (新しいウィンドウを開きます)Packのデフォルト関数について調べます。

state.rs行われ、のがに戻ってみようprocessor.rsと、私たちの1に調整use文を。

から

use crate::{instruction::EscrowInstruction, error::EscrowError};
1

use crate::{instruction::EscrowInstruction, error::EscrowError, state::Escrow};
1

プロセッサパート2、PDAパート2、CPIパート1

process_init_escrow最初に状態のシリアル化を追加して、関数を終了しましょう。すでにエスクロー構造体インスタンスを作成し、それが実際に初期化されていないことを確認しました。構造体のフィールドにデータを入力する時が来ました!

// inside process_init_escrow
...
let mut escrow_info = Escrow::unpack_unchecked(&escrow_account.try_borrow_data()?)?;
if escrow_info.is_initialized() {
    return Err(ProgramError::AccountAlreadyInitialized);
}

escrow_info.is_initialized = true;
escrow_info.initializer_pubkey = *initializer.key;
escrow_info.temp_token_account_pubkey = *temp_token_account.key;
escrow_info.initializer_token_to_receive_account_pubkey = *token_to_receive_account.key;
escrow_info.expected_amount = amount;

Escrow::pack(escrow_info, &mut escrow_account.try_borrow_mut_data()?)?;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

かなり簡単です。pack関数を内部的に呼び出すもう1つのデフォルト関数ですpack_into_slice

PDAパート2

内部で行うべきことが1つ残っていますprocess_init_escrow。それは、一時トークンアカウントの所有権(ユーザースペース)をPDAに譲渡することです。これは、PDAが実際に何であるか、およびprogram_idプロセス関数の内部が必要になる理由を説明する良い機会です。ハイライトされた行をコピーして見てください。

// inside process_init_escrow
...
escrow_info.expected_amount = amount;
Escrow::pack(escrow_info, &mut escrow_account.try_borrow_mut_data()?)?;
let (pda, _bump_seed) = Pubkey::find_program_address(&[b"escrow"], program_id);
1
2
3
4
5

シードの配列とを関数に渡すことにより、PDAを作成program_idfind_program_addressます。新しいものpdaを取り戻し、bump_seed(アリスのテキサス州ではバンプシードは必要ありません)1 /(2 ^ 255)の確率で関数が失敗します(2 ^ 255は大きな数です) (新しいウィンドウを開きます))。私たちの場合、シードは静的である可能性があります。関連トークンアカウントプログラムなどの場合があります (新しいウィンドウを開きます)そうでない場合(異なるユーザーが異なる関連トークンアカウントを所有する必要があるため)。必要なのは、同じ時点で発生するさまざまなエスクローの一時的なトークンアカウントを1所有できるPDAだけですN

わかりました、しかしPDAとは何ですか?通常、Solanaキーペアはed25519 (新しいウィンドウを開きます)標準。これは、通常の公開鍵がed25519楕円曲線上にあることを意味します。PDAは、program_idとシードから派生した公開鍵であり、バンプシードによってカーブから押し出されたものです。したがって、

プログラム派生アドレスはed25519曲線上にないため、秘密鍵が関連付けられていません。

PDAはバイトのランダムな配列であり、唯一の定義機能は、それらがその曲線上にないことです。とはいえ、ほとんどの場合、通常のアドレスとして使用できます。PDAに関する2つの異なるドキュメントを絶対に読む必要があります(ここ (新しいウィンドウを開きます)そしてここ(find_program_addressはこの関数を呼び出します) (新しいウィンドウを開きます))。ここではまだバンプシードを使用していません(変数名の前にアンダースコアも示されています)。ボブのトランザクション内のPDAパート3で秘密鍵がなくても、PDAを使用してメッセージに署名する方法を調べるときにこれを行います。

CPIパート1

今のところ、一時トークンアカウントの(ユーザースペース)所有権をPDAに譲渡する方法を見てみましょう。これを行うには、エスクロープログラムからトークンプログラムを呼び出します。これは、クロスプログラム呼び出しと呼ばれます (新しいウィンドウを開きます)invokeまたはinvoke_signed関数のいずれかを使用して実行されます。ここではを使用しますinvoke。ボブのトランザクションでは、を使用しますinvoke_signed。その時、違いが明らかになります。invoke命令とアカウントの配列の2つの引数を取ります。

use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint::ProgramResult,
    program_error::ProgramError,
    msg,
    pubkey::Pubkey,
    program_pack::{Pack, IsInitialized},
    sysvar::{rent::Rent, Sysvar},
    program::invoke
};
// inside process_init_escrow
...
let token_program = next_account_info(account_info_iter)?;
let owner_change_ix = spl_token::instruction::set_authority(
    token_program.key,
    temp_token_account.key,
    Some(&pda),
    spl_token::instruction::AuthorityType::AccountOwner,
    initializer.key,
    &[&initializer.key],
)?;

msg!("Calling the token program to transfer token account ownership...");
invoke(
    &owner_change_ix,
    &[
        temp_token_account.clone(),
        initializer.clone(),
        token_program.clone(),
    ],
)?;

Ok(())
// end of process_init_escrow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

solana_programuseステートメントをコピーして置き換えます。続いprocess_init_escrowて、token_programアカウントを取得します。CPIを介して呼び出されるプログラムは、invoke(およびinvoke_signed)の2番目の引数にアカウントとして含める必要があるという規則があります。次に、命令を作成します。これは、通常の呼び出しを実行した場合にトークンプログラムが期待する命令にすぎません。トークンプログラムは、その中にinstruction.rs使用できるいくつかのヘルパー関数を定義します。私たちにとって特に興味深いのはset_authorityそのような命令を作成するためのビルダー関数である関数。トークンプログラムIDを渡し、次に権限を変更するアカウント、新しい権限であるアカウント(この場合はPDA)、権限変更のタイプ(トークンアカウントにはさまざまな権限タイプがあります)を渡します。所有者の変更に注意してください)、現在のアカウント所有者(Alice-> initializer.key)、そして最後にCPIに署名する公開鍵。

ここで使用されている概念はSignatureExtensionです (新しいウィンドウを開きます)。要するに、

含む場合にはsignedプログラム呼び出しでアカウント、現在の命令の内側にそのプログラムによって作られ、そのアカウントを含むすべてのCPIには、アカウントにもなりますsignedつまり、署名が延長されるのCPIへ。

私たちの場合、これは、アリスがInitEscrowトランザクションに署名したため、プログラムがトークンプログラムをset_authorityCPIにし、署名者のパブキーとして彼女のパブキーを含めることができることを意味します。トークンアカウントの所有者を変更するには、もちろん現在の所有者の承認が必要になるため、これが必要です。

命令の横に、呼び出しているプログラムのアカウントに加えて、命令に必要なアカウントも渡す必要があります。これらを調べるには、トークンプログラムに移動しinstruction.rs、必要なアカウント(この場合は現在の所有者のアカウントと所有者を変更するアカウント)をコメントで示すsetAuthority列挙型を見つけます。

CPIを作成する前に、token_programが本当にトークンプログラムのアカウントであるという別のチェックを追加する必要があることに注意してください。それ以外の場合は、不正なプログラムを呼び出している可能性があります。spl-token上記のバージョンのクレート3.1.1(このガイドで使用しています)を使用している場合、それらの命令ビルダー関数を使用すれば、これを行う必要はありません。彼らはあなたのためにそれをします。

最後に、entrypoint.rs次のように調整します。

use solana_program::{
    account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey
};

use crate::processor::Processor;

entrypoint!(process_instruction);
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    Processor::process(program_id, accounts, instruction_data)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

今、あなたは「待ってください、私たちは一時的なアカウントに対するエスクロープログラムの制御を与えていますが、アリスはそれに何も転送しませんでした!それは以前に起こったはずではありませんか?」と考えているかもしれません。そして、あなたは正しいです、それは前に起こるべきです!しかし重要なことに、それはエスクロープログラム自体の内部で発生する必要はありません。次のセクションでは、彼女がプログラムの呼び出しと同じtxで転送を行う方法を調べます。

理論の要約📚
    • プログラム派生アドレスはed25519曲線上にないため、秘密鍵が関連付けられていません。
    • 含む場合にはsignedプログラム呼び出しでアカウント、現在の命令の内側にそのプログラムによって作られ、そのアカウントを含むすべてのCPIには、アカウントにもなりますsignedつまり、署名が延長されるのCPIへ。

プログラムを試して、アリスの取引を理解する

それ自体で完全なプログラムの一部を構築したので、これで試してみることができます!そうすることで、Solanaについてより多くの知識を得ることができます。たとえば、アカウントはどこから来たのですか?

プログラムを操作する方法を2つ用意しました。両方を試してみることをお勧めします。まず、プログラムを正しく実装したかどうかをテストする一連のスクリプトがあります。それらを読むことで、クライアント側でsolanaプログラムと対話する方法についての理解を深めることもできます。スクリプトとその使用方法に関する説明は、ここにあります。 (新しいウィンドウを開きます)

あなたは(あなたのプログラムを試してみることも、このUIを使用することができます。ここの (新しいウィンドウを開きます)コード)。スクリプトを実行するよりも少し複雑ですが、スクリプトがどのように機能するか、およびスクリプトを機能させるために何をする必要があるかを以下に説明します。自由に自分で作成してください!

ローカルネットにプログラムをデプロイする

まず、cargo build-bpfコマンドを使用して、プログラムをsoファイル拡張子の付いたファイルにコンパイルします。

実行solana-keygen newして、solanaキーペアをローカルに作成して保存します。(またはCLIウォレットを作成します (新しいウィンドウを開きます)あなたが選んだものです。)

コマンド(これでPATHに含まれているはずです)を使用してローカルネットを起動しますsolana-test-validator。を呼び出すときsolana config get、「RPCURL」はに等しくなりhttp://localhost:8899ます。そうでない場合は、を実行しsolana config set --url http://localhost:8899ます。実行solana balanceすると、0であってはならない残高が表示されます。そうである場合は、バリデーターを停止し、でキーを作成したことを確認solana-keygen newし、でジェネシスから再開しsolana-test-validator -rます。

次に、solana deployコマンドを使用してプログラムをlocalnetに展開します。プログラムへのパスはによって出力されcargo build-bpfます。(⚠️現在、どのクラスターでも有効になっていない機能を使用しているため、プログラムをローカルに展開することしかできません)

solana deploy PATH_TO_YOUR_PROGRAM
1

deployコマンドは、あなたが今、上記のUIに貼り付けることができますプログラムIDを印刷する必要があります。

使い捨ての秘密鍵を作成する

私のUIには秘密鍵が必要です(実際のアプリでは絶対にこれを行わないでください)。sollet.ioに移動します (新しいウィンドウを開きます)まったく新しいアカウントまたはメインアカウントの横に新しいアカウントを作成します。このアカウントはアリスを表します。

solletクラスターをlocalnetに変更します。

ウォレットを作成したら、SOLをエアドロップして、TX料金を支払います。次に、画面の中央にあるSOLアカウントをクリックしてexportから、秘密鍵のバイト配列をエクスポートして上に貼り付けます。

次のステップにも使い捨ての財布を使用してください。

ローカルネットでテストするためのトークンの作成

エスクローに入れるトークンも必要になるので、SPLトークンUIにアクセスしてください (新しいウィンドウを開きます)。ここでもlocalnetを選択してください。

次の手順では、XとYのトークンミントアカウントと、アリスのXアカウントとアリスのYアカウントの2つのトークンアカウントを作成します。各トークンアカウント(トークンミントアカウントではありません!)を作成したら、アカウントアドレスをコピーして、適切なUIフィールドに入力します。また、それらを別の場所に書き留めて、ボブのトランザクションを含むエスクロー全体を最終的にテストするときに再利用できるようにすることもできます。

タブのCreate new token内側に移動することから始めTokensます。ミントオーソリティにあなたのsolletpubkeyを記入し、新しいトークンを作成します。これはトークンXのトークンミントアカウントです。つまり、トークンのすべてのメタデータ(たとえば、その供給とミントを許可されている人)を保持するアカウントです(ミント権限を正しく設定した場合は、それがsolletpubkeyになります)。 !これはエクスプローラーで確認できます)。

次に、タブCreate account内に移動し、Accounts作成したトークンのアドレスを入力して、アカウント所有者としてsolletpubkeyを使用します。アカウントを作成します(関連付けられているかどうかは関係ありません)。これはアリスのトークンXトークンアカウントです。

次に、タブのEdit account内側に移動しAccountsます。このmintオプションはデフォルトで選択されています。アリスのトークンXトークンアカウント(作成したばかりのアカウント)を宛先アカウントとして入力し、金額フィールドにいくつかの番号を入力します。をクリックしMint to accountます。

トークンYについても同じ手順を実行します。トークンをアリスのトークンYアカウントに作成する必要はありません。

エスクローの作成

すべての手順が完了したら、あとはアリスの予想金額とエスクローに入れたい金額を入力するだけです。両方の数字を入力し(2番目はアリスのアカウントに作成した数字よりも小さい必要があります)、を押しますInit Escrow

何が起こったのかを理解する、パート2を借りる、そしてコミットメント

アリスが送信するトランザクションの寿命を示す小さなスライドショーを作成しました。画像が複雑になりすぎないように、内部とユーザースペースの関係の矢印は省略しました。

右上にあるように、

Solanaの1つのトランザクション(tx)内に複数の命令(ix)が存在する可能性があります。これらの命令は同期的に実行され、txは全体としてアトミックに実行されます。これらの命令は、さまざまなプログラムを呼び出すことができます。

これは、1つの命令が失敗すると、トランザクション全体が失敗することを意味します。ix1で、アカウントがどのように実現するかを確認できます。

システムプログラムは、アカウントスペースの割り当てと、(ユーザースペースではなく内部の)アカウント所有権の割り当てを担当します。

アリスのトランザクションは5つの命令で構成されています。

1. create empty account owned by token program
2. initialize empty account as Alice's X token account
3. transfer X tokens from Alice's main X token account to her temporary X token account
4. create empty account owned by escrow program
5. initialize empty account as escrow state and transfer temporary X token account ownership to PDA
1
2
3
4
5

ご覧のように、

命令は、同じトランザクション内の以前の命令に依存する場合があります

ここで、Solana js / tsライブラリを使用するフロントエンドコードの重要な部分について説明します。コードを自由に見てください (新しいウィンドウを開きます)あなた自身。

const tempTokenAccount = new Account();
const createTempTokenAccountIx = SystemProgram.createAccount({
    programId: TOKEN_PROGRAM_ID,
    space: AccountLayout.span,
    lamports: await connection.getMinimumBalanceForRentExemption(AccountLayout.span, 'confirmed'),
    fromPubkey: feePayerAcc.publicKey,
    newAccountPubkey: tempTokenAccount.publicKey
});
1
2
3
4
5
6
7
8

作成される最初の命令は、最終的にPDAに転送される新しいXトークンアカウントを作成することです。ここで作成されたばかりで、まだ何も送信されていないことに注意してください。この関数では、ユーザーが新しいアカウントが属するプログラム(programId)、アカウントに必要なスペース(space)、初期残高(lamports)、残高の転送元(fromPubkey)、および新しいアカウントのアドレスを指定する必要があります。アカウント(newAccountPubkey)。

およそ何'confirmed'引数?confirmed利用可能なコミットメントの1つです (新しいウィンドウを開きます)ネットワークにクエリを実行する方法を教えてくれます。どのコミットメントレベルを選択するかは、ユースケースによって異なります。数百万を移動していて、txがロールバックできないことを可能な限り確認したい場合は、を選択しますfinalized楽観的な確認とスラッシュのconfirmedため、まだかなり安全です (新しいウィンドウを開きます)

const initTempAccountIx = Token.createInitAccountInstruction(TOKEN_PROGRAM_ID, XTokenMintAccountPubkey, tempTokenAccount.publicKey, feePayerAcc.publicKey);
const transferXTokensToTempAccIx = Token
    .createTransferInstruction(TOKEN_PROGRAM_ID, initializerXTokenAccountPubkey, tempTokenAccount.publicKey, feePayerAcc.publicKey, [], amountXTokensToSendToEscrow);
1
2
3

新しいアカウントを作成するためのixを構築した後、spl-tokenjsライブラリによって提供される2つの関数を呼び出します (新しいウィンドウを開きます)次の2つの命令を作成します。ここでは何も新しいことはありません。次に、命令4は別のアカウントを作成しています。今回はエスクロープログラムが所有していますが、最初のixと非常によく似ています。

const initEscrowIx = new TransactionInstruction({
    programId: escrowProgramId,
    keys: [
        { pubkey: initializerAccount.publicKey, isSigner: true, isWritable: false },
        { pubkey: tempTokenAccount.publicKey, isSigner: false, isWritable: true },
        { pubkey: new PublicKey(initializerReceivingTokenAccountPubkeyString), isSigner: false, isWritable: false },
        { pubkey: escrowAccount.publicKey, isSigner: false, isWritable: true },
        { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
        { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
    ],
    data: Buffer.from(Uint8Array.of(0, ...new BN(expectedAmount).toArray("le", 8)))
})
1
2
3
4
5
6
7
8
9
10
11
12

ここではソラナライブラリからほとんど助けを得られないので、5番目で最後のix(エスクローを開始する場所)はより興味深いものです。コンストラクター(new TransactionInstruction...)を呼び出して、手動で命令を作成します。必要なフォーマットはなじみ深いはずです!それはまさに私たちのプログラムエントリポイントが期待するものです。エスクロープログラムのprogramIdを渡し、次にキーを渡します。ここでは、特定のアカウントがtxに署名するかどうか(署名しない場合はtxが失敗するか)、アカウントが読み取り専用かどうかを指定します。その後、txに書き込まれる場合は失敗します。最後に、エントリポイントに到着するものをとして指定しますinstruction_data。私たちは、で始まる0最初のバイトは、私たちが使用したものであるため、instruction.rstagの命令をデコードする方法を決定します。0を意味しInitEscrowます。次のバイトはexpected_amountbn.jsライブラリを使用して、予想される量をリトルエンディアンの数値の8バイト配列として書き込みます。内部でinstruction.rsはau64とリトルエンディアンをデコードするため、8バイトu64::from_le_bytesです。これは、スライスをでデコードするためです。トークンの最大供給量であるu64ため、を使用します (新しいウィンドウを開きます)

const tx = new Transaction()
        .add(createTempTokenAccountIx, initTempAccountIx, transferXTokensToTempAccIx, createEscrowAccountIx, initEscrowIx);
await connection.sendTransaction(tx, [initializerAccount, tempTokenAccount, escrowAccount], {skipPreflight: false, preflightCommitment: 'confirmed'});
1
2
3

最後に、新しいトランザクションを作成し、すべての命令を追加します。次に、署名者と一緒にtxを送信します。jsライブラリの世界では、anAccountには二重の意味があり、キーペアを保持するためのオブジェクトとしても使用されます。つまり、渡す署名者には秘密鍵が含まれており、実際に署名することができます。明らかに、署名者としてアリスのアカウントを追加する必要があります。彼女は料金を支払い、アカウントからの送金を承認する必要があります。システムプログラムが新しいアカウントを作成するときに、txがそのアカウントによって署名される必要があることが判明したため、他の2つのアカウントも追加する必要があります。

アリスの取引の後に私たちが最終的に得たのは最後のスライドです。取引を完了するための関連データを保持する新しいエスクロー状態アカウントと、エスクロープログラムのPDAが所有する新しいトークンアカウントがあります。そのトークンアカウントのトークン残高は、アリスがYトークンの予想される量(エスクロー状態のカウントに保存される)と交換したいXトークンの量です。

ここで重要な注意点は、すべての命令が同じトランザクションにあることは重要ではありませんが、少なくともix1,2とix4,5が同じトランザクションにあることが重要であるということです。これは、システムプログラムによってアカウントが作成された後、アカウントがブロックチェーン上に浮かんでいるだけで、まだ初期化されておらず、ユーザースペースの所有者がいないためです。たとえば、ix 1と2を異なるトランザクションに配置した場合、誰かがそれら2つの間にtxを送信し、ix 1によって作成された当時の所有者のいないアカウントを使用して自分のトークンアカウントを初期化しようとする可能性があります。これは、ixを配置した場合は発生しません。 txはアトミックに実行されるため、同じトランザクションで1と2。

フロントエンドを実際の使用に適合させる

省略されたものがいくつかあります-物事を単純にするために-しかし、実際のプログラムには間違いなく追加する必要があります。まず、トークンの最大量はU64_MAXであり、javascriptの数値よりも高くなっています。したがって、これを処理する方法を見つける必要があります。これは、挿入できるトークンの許容量を制限するか、トークンの量を文字列として受け入れてbn.jsから、文字列を変換するようなライブラリを使用することによって行います。次に、ユーザーに秘密鍵を入れてはいけません。solongまたはsol-wallet-adapterライブラリのような外部ウォレットを使用します。トランザクションを作成し、手順を追加してから、使用している信頼できるサービスにトランザクションに署名して返送するよう依頼します。次に、他の2つのキーペアアカウントを追加して、txをネットワークに送信できます。

理論の要約📚
    • Solanaの1つのトランザクション(tx)内に複数の命令(ix)が存在する可能性があります。これらの命令は同期的に実行され、txは全体としてアトミックに実行されます。これらの命令は、さまざまなプログラムを呼び出すことができます。
    • システムプログラムは、アカウントスペースの割り当てと、(ユーザースペースではなく内部の)アカウント所有権の割り当てを担当します。
    • 指示は、同じトランザクション内の以前の指示に依存する場合があります
    • コミットメント設定により、ダウンストリームの開発者は、ファイナリティの可能性が異なるネットワークにクエリを実行できます。

エスクロープログラムの構築-ボブのトランザクション

アリスがエスクローを作成した後、彼女はエスクロー状態のアカウントアドレスをボブに送信できます。予想される量のYトークンをエスクローに送信すると、エスクローは彼にアリスのXトークンとアリスのYトークンを送信します。次に、エスクロープログラムをボブのトランザクションに対応できるようにする方法を説明します。ガイドのこの第2部では、Solanaの概念についても学ぶことができます。すでに作成したコードの多くを再利用できるため、この部分はかなり短くなります。作成する必要のあるコードにはあまり時間をかけませんが、その方法はすでに知っています。

Instruction.rsパート3、ボブのトランザクションが何をすべきかを理解する

ボブのトランザクションが何をすべきかを理解するために、アリスのトランザクションが完了した後の状態をもう一度見てみましょう。

アリスとボブの間の取引に必要なすべての情報を保持するエスクローアカウントがあり、アリスのXトークンを保持し、エスクロープログラムのPDAが所有するトークンアカウントもあることがわかります。 )txに実行させたいのは、XトークンをPDAが所有するXトークンアカウントから彼のXトークンアカウントに移動することです。エスクロープログラムは、ボブのYトークンアカウントからトークンを差し引き、それらをYトークンアカウントに追加する必要があります。アリスは、エスクロープログラムにエスクロー状態アカウント(initializer_token_to_receive_account_pubkey内部のエスクロー構造体内のプロパティ)に書き込みさせましたstate.rs。最後に、取引用に作成された2つのアカウント(エスクロー状態アカウントと一時的なXトークンアカウント)は、もう必要ないため、クリーンアップする必要があります。

この知識があれば、Exchange内部の命令と呼ぶことにしたもののエンドポイントを追加できますinstruction.rs

/// Accepts a trade
///
///
/// Accounts expected:
///
/// 0. `[signer]` The account of the person taking the trade
/// 1. `[writable]` The taker's token account for the token they send 
/// 2. `[writable]` The taker's token account for the token they will receive should the trade go through
/// 3. `[writable]` The PDA's temp token account to get tokens from and eventually close
/// 4. `[writable]` The initializer's main account to send their rent fees to
/// 5. `[writable]` The initializer's token account that will receive tokens
/// 6. `[writable]` The escrow account holding the escrow info
/// 7. `[]` The token program
/// 8. `[]` The PDA account
Exchange {
    /// the amount the taker expects to be paid in the other token, as a u64 because that's the max possible supply of a token
    amount: u64,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ixには合計9つのアカウントが必要です。内部でどのように使用されているかがわかるので、説明は省略processor.rsします。これで、自分でこれを理解できるようになります。

重要なのは、ixも金額を期待していることです。なぜこれが必要なのですか?結局のところ、ボブはエクスプローラーでエスクロー情報アカウントを検索し、その状態で、Yトークンに対して快適に受信できるXトークンの量を持つ一時トークンアカウントへの参照があることを確認できます。なぜ彼が期待する金額がまだ彼のixに含まれている必要があるのですか?自分で理解してみてください!

最初のヒントを表示
2番目のヒントを表示する
解決策を示す

また、unpack関数の一致式を調整して、新しいフィールドを含める必要があります。

// inside unpack
Ok(match tag {
    0 => Self::InitEscrow {
        amount: Self::unpack_amount(rest)?,
    },
    1 => Self::Exchange {
        amount: Self::unpack_amount(rest)?
    },
    _ => return Err(InvalidInstruction.into()),
})
1
2
3
4
5
6
7
8
9
10

以上ですinstruction.rs。この時点で、残りの部分は演習として自分で試して終了することができますが、もちろん、ガイドを続けることもできます。最初に自分で試してみることにした場合は、必ずinvoke_signed関数を調べてください。XトークンをBobに転送するときに必要になります。さらに、取引用に作成されたアカウントを何らかの方法でクリーンアップする必要があることに注意してください。

プロセッサパート3、PDAパート3

一致は網羅的である必要があるため、つまり列挙型のすべてのバリアントに一致する必要があるため、process内部の関数processor.rsはコンパイルされません。調整しましょう。

match instruction {
    EscrowInstruction::InitEscrow { amount } => {
        msg!("Instruction: InitEscrow");
        Self::process_init_escrow(accounts, amount, program_id)
    },
    EscrowInstruction::Exchange { amount } => {
        msg!("Instruction: Exchange");
        Self::process_exchange(accounts, amount, program_id)
    }
}
1
2
3
4
5
6
7
8
9
10

次に、process_exchangeここで参照する関数を作成します。

// inside: impl Processor {}
fn process_exchange(
    accounts: &[AccountInfo],
    amount_expected_by_taker: u64,
    program_id: &Pubkey,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let taker = next_account_info(account_info_iter)?;

    if !taker.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let takers_sending_token_account = next_account_info(account_info_iter)?;

    let takers_token_to_receive_account = next_account_info(account_info_iter)?;

    let pdas_temp_token_account = next_account_info(account_info_iter)?;
    let pdas_temp_token_account_info =
        TokenAccount::unpack(&pdas_temp_token_account.try_borrow_data()?)?;
    let (pda, bump_seed) = Pubkey::find_program_address(&[b"escrow"], program_id);

    if amount_expected_by_taker != pdas_temp_token_account_info.amount {
        return Err(EscrowError::ExpectedAmountMismatch.into());
    }

    let initializers_main_account = next_account_info(account_info_iter)?;
    let initializers_token_to_receive_account = next_account_info(account_info_iter)?;
    let escrow_account = next_account_info(account_info_iter)?;

    let escrow_info = Escrow::unpack(&escrow_account.try_borrow_data()?)?;

    if escrow_info.temp_token_account_pubkey != *pdas_temp_token_account.key {
        return Err(ProgramError::InvalidAccountData);
    }

    if escrow_info.initializer_pubkey != *initializers_main_account.key {
        return Err(ProgramError::InvalidAccountData);
    }

    if escrow_info.initializer_token_to_receive_account_pubkey != *initializers_token_to_receive_account.key {
        return Err(ProgramError::InvalidAccountData);
    }

    let token_program = next_account_info(account_info_iter)?;

    let transfer_to_initializer_ix = spl_token::instruction::transfer(
        token_program.key,
        takers_sending_token_account.key,
        initializers_token_to_receive_account.key,
        taker.key,
        &[&taker.key],
        escrow_info.expected_amount,
    )?;
    msg!("Calling the token program to transfer tokens to the escrow's initializer...");
    invoke(
        &transfer_to_initializer_ix,
        &[
            takers_sending_token_account.clone(),
            initializers_token_to_receive_account.clone(),
            taker.clone(),
            token_program.clone(),
        ],
    )?;
    Ok(())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

この時点まで、本当に新しいことは何もありません。アカウントを取得し、いくつかのチェックを行って、ボブが実際に正しい値で正しいアカウントを渡したこと、およびPDAのXトークンアカウントの金額がボブが期待するものであることを確認します。次に、署名拡張機能を使用して、ボブに代わってアリスのYトークンアカウントにトークンを転送します。今すぐコンパイルエラーを自分で修正できます(spl_tokenのアカウント構造体をインポートして名前を変更する必要があります (新しいウィンドウを開きます)にそれをTokenAccount追加し、別のエラーを追加します)。の新しいエラーバリアントを作成しExpectedAmountMismatch、必要なモジュールをuse。でスコープにプルします。

process_exchange関数の最後の部分には、再び何か新しいものが含まれています。

...
let pda_account = next_account_info(account_info_iter)?;

let transfer_to_taker_ix = spl_token::instruction::transfer(
    token_program.key,
    pdas_temp_token_account.key,
    takers_token_to_receive_account.key,
    &pda,
    &[&pda],
    pdas_temp_token_account_info.amount,
)?;
msg!("Calling the token program to transfer tokens to the taker...");
invoke_signed(
    &transfer_to_taker_ix,
    &[
        pdas_temp_token_account.clone(),
        takers_token_to_receive_account.clone(),
        pda_account.clone(),
        token_program.clone(),
    ],
    &[&[&b"escrow"[..], &[bump_seed]]],
)?;

let close_pdas_temp_acc_ix = spl_token::instruction::close_account(
    token_program.key,
    pdas_temp_token_account.key,
    initializers_main_account.key,
    &pda,
    &[&pda]
)?;
msg!("Calling the token program to close pda's temp account...");
invoke_signed(
    &close_pdas_temp_acc_ix,
    &[
        pdas_temp_token_account.clone(),
        initializers_main_account.clone(),
        pda_account.clone(),
        token_program.clone(),
    ],
    &[&[&b"escrow"[..], &[bump_seed]]],
)?;

Ok(())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

ここではinvoke_signed、PDAが何かに署名できるようにする関数を使用します。PDAがEd25519楕円曲線からぶつかったことを思い出してください。したがって、秘密鍵はありません。問題は、PDAがCPIに署名できるかどうかです。そして答えは

PDAは、実際には暗号化された方法でCPIに署名していません。2つの引数に加えて、invoke_signed関数は3番目の引数を取ります。CPIが「署名」されることになっているPDAを作成するために使用されたシードです。シードとして定義しなかったので、そこにバンプシードを見つけて驚かれるかもしれません。さて、バンプシードはfind_program_address、アドレスをEd25519曲線から外すために関数が追加するシードです。今、

プログラムがを呼び出すinvoke_signedと、ランタイムはそれらのシードと呼び出し元プログラムのプログラムIDを使用してPDAを再作成し、invoke_signed引数内の指定されたアカウントの1つと一致する場合、そのアカウントのsignedプロパティはtrueに設定されます。

どのプログラムがinvoke_signed呼び出しを行っているかを確認するのはランタイムであるため、他のプログラムはこのPDAを偽造できません。エスクロープログラムのみがprogramIdを持ち、その結果、PDAはのinvoke_signedaccounts引数のアドレスの1つに等しくなります。

これで、最初のinvoke_signed呼び出しでトークンが一時XトークンアカウントからボブのメインXトークンアカウントに転送されることがわかります。2つ目は、アカウントを閉鎖します。これは何を意味するのでしょうか?家賃を免除するには、アカウントに最低残高が必要であることを思い出してください。アカウントが不要になったときにその残高を取り戻すことができたら素晴らしいと思いませんか?結局のところ、残高を別の口座に送金するのと同じくらい簡単です。

アカウントに残高が残っていない場合、トランザクション後のランタイムによってメモリから削除されます(エクスプローラーで閉じられたアカウントに移動すると、これを確認できます)

これで、エスクロー状態のアカウントが家賃免除であるかどうかを確認する必要があった理由がわかります。もし私たちがそうしなかったとしたら、アリスが家賃を免除されていない口座を渡すことになった場合、ボブが取引を行う前に口座残高がゼロになる可能性があります。アカウントがなくなると、アリスはトークンを回復する方法がなくなります。

一時トークンアカウントはトークンプログラムによって所有されているため、トークンプログラムのみが残高を減らすことができます。また、このアクションにはトークンアカウント(この場合はPDA)の(ユーザースペース)所有者の許可が必要なため、invoke_signed再度使用します。

この機能とプログラムを完了するために、エスクロー状態のアカウントで同じことを行うことができます。

msg!("Closing the escrow account...");
**initializers_main_account.lamports.borrow_mut() = initializers_main_account.lamports()
.checked_add(escrow_account.lamports())
.ok_or(EscrowError::AmountOverflow)?;
**escrow_account.lamports.borrow_mut() = 0;
*escrow_account.try_borrow_mut_data()? = &mut [];

Ok(())
1
2
3
4
5
6
7
8

ランポーツの転送は、一方のアカウントに金額を加算し、もう一方のアカウントから減算するのと同じくらい簡単です。私たちは、私たちがしているので、エスクロープログラムは彼女のアカウントの所有者でなくても(彼女は2つのアカウントは、貿易のために必要な作成したので、彼女はlamportsを取得する必要があります)アリスのメイン口座の残高を調整することができますクレジット彼女の口座にlamportsを。ここでも、新しいエラーをに追加しますerror.rs

また、データフィールドを空のスライスに等しく設定していることに注意してください。とにかくトランザクション後にアカウントがメモリから削除された場合、なぜこれが必要なのですか?これは、この命令が必ずしもトランザクションの最後の命令ではないためです。したがって、後続のトランザクションでは、アカウントを再度賃貸料免除にすることで、データを読み取ったり、完全に復活させたりすることができます。プログラムによっては、データフィールドをクリアするのを忘れると、危険な結果を招く可能性があります。

「近い」種類のプログラムを呼び出す場合、つまり、アカウントのランプをゼロに設定して、トランザクション後にメモリから削除する場合は、データフィールドをクリアするか、データを次のような状態のままにしてください。後続のトランザクションで回復しても問題ありません。

以上です!それがプログラムです。素晴らしい!

理論の要約📚
    • プログラムがを呼び出すinvoke_signedと、ランタイムは指定されたシードと呼び出し元プログラムのプログラムIDを使用してPDAを再作成し、invoke_signed引数内の指定されたアカウントの1つと一致する場合、そのアカウントのsignedプロパティはtrueに設定されます。
    • アカウントに残高が残っていない場合、トランザクション後のランタイムによってメモリから削除されます(エクスプローラーで閉じられたアカウントに移動すると、これを確認できます)
    • トランザクション後にアカウントをメモリから削除することを目的としている場合でも、「閉じる」命令はデータフィールドを適切に設定する必要があります

プログラムを試して、実際のボブのトランザクションを理解する

これで、プログラム全体を試すことができます。このために、以下のアリスのUIをコピーしたので、上にスクロールしてボブの側に別のUIを追加する必要はありません。

アリスのUI 

ボブのUI 

更新されたプログラムをビルドしてデプロイする必要があります(これは、古いバージョンのプログラムが所有しているため、しばらく前に作成したエスクローアカウントを使用できないことも意味します)。また、現実的なテストのために、ボブとして機能する別のアカウントをsolletに作成します。少し前に作成した2つのトークンミントアカウントを再利用できます。ボブのXトークンとYトークンを保持する2つの新しいトークンアカウントを作成します。これで、アリスとして新しいエスクローを作成し、ボブとして取引を受け入れることができます。

何が起こったのかを理解する

「トレードする」をクリックすると、舞台裏で何が起こったのかがすでにわかります。UIは、エスクローアカウントpubkeyを使用してエスクローアカウントからデータを取得し、それをデコードしてから、デコードされたデータとボブのデータを使用してトランザクションを送信します。重要なコードは次のとおりです(デコードなし):

const PDA = await PublicKey.findProgramAddress([Buffer.from("escrow")], programId);

const exchangeInstruction = new TransactionInstruction({
    programId,
    data: Buffer.from(Uint8Array.of(1, ...new BN(takerExpectedXTokenAmount).toArray("le", 8))),
    keys: [
        { pubkey: takerAccount.publicKey, isSigner: true, isWritable: false },
        { pubkey: takerYTokenAccountPubkey, isSigner: false, isWritable: true },
        { pubkey: takerXTokenAccountPubkey, isSigner: false, isWritable: true },
        { pubkey: escrowState.XTokenTempAccountPubkey, isSigner: false, isWritable: true},
        { pubkey: escrowState.initializerAccountPubkey, isSigner: false, isWritable: true},
        { pubkey: escrowState.initializerYTokenAccount, isSigner: false, isWritable: true},
        { pubkey: escrowAccountPubkey, isSigner: false, isWritable: true },
        { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false},
        { pubkey: PDA[0], isSigner: false, isWritable: false}
    ] 
})

await connection.sendTransaction(new Transaction().add(exchangeInstruction), [takerAccount], {skipPreflight: false, preflightCommitment: 'confirmed'});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

ボーナス:バグ修正!

このプログラムにはバグがあります。それは重要なことではありません。Solanaでのプログラミングの微妙さを示しているので、そのままにしておきました。あなたはそれを見つけることができますか?

最初のヒントを表示
2番目のヒントを表示する
解決策を示す

Q&A

これは、ガイド自体が長すぎると思われる回答の読者から寄せられた質問のコレクションです。お気軽にご 連絡ください (新しいウィンドウを開きます)離れて聞いてください!

アリスのXトークンの一時的なアカウントが本当に必要ですか?

Q:アリスのXトークンの一時的なアカウントは本当に必要ですか?アリスがエスクロー状態内で取引したいXトークンの量を節約し、ボブのトランザクション内で、エスクロープログラムにトークンプログラムへのCPIを作成させて、その量を彼女のアカウントから差し引くことはできませんか?

Afromトークン転送のアドレスがトランザクションに署名する必要があるため、これは機能しません。ボブはアリスにトランザクションに署名するように依頼できますが、それではアリスとボブの間の通信がさらに必要になり、ユーザーエクスペリエンスが低下します。使用できるのはトークンアカウントの(ユーザースペース)delegateプロパティですが、トークンアカウントは1つのデリゲートしか持てません。つまり、アリスは一度に1つの継続的なエスクローしか持てません。

潜在的な改善

ユーザーエクスペリエンスを向上させるためのいくつかのアイデアがあります

  • より良いUIを構築する
  • Cancelプログラムにエンドポイントを追加します。現在、アリスのトークンは行き詰まっており、ボブが取引を行わないことを決定した場合、彼女はそれらを回復することができません。アリスが進行中のエスクローをキャンセルできるようにするエンドポイントを追加し、Xトークンを彼女に戻し、作成された2つのアカウントを閉じます。🚨キャンセルを実装する場合は、フロントランニング攻撃を防ぐために別のチェックも追加する必要があります。それを防ぐには、ボブがアリスexpected_y_amountに期待するXトークンの量に加えて、アリス()に送信することを期待するYトークンの量も送信する必要があります。小切手はに属し、process_exchangeそれを確認しますescrow_info.expected_amount == expected_y_amount。これにより、次の攻撃が防止されます。ボブがトランザクションを送信し、アリスがそれを確認すると、アリスはエスクローをキャンセルし、同じアドレスで、予想される金額が多い状態で再初期化できるため、ボブが予想したよりも多くのYトークンを受け取ることができます。あるいは(または追加で)、ボブは自分で一時トークンアカウントを使用して、アリスが彼をフロントランした場合、一時トークンアカウントに十分なYトークンがないためにtxが失敗するようにすることができます。さらに、ボブに送信してもらう必要もありますexpected_x_amount#instruction-rs-part-3-understanding-what-bob-s-transaction-should-doのフロントランニングセクションを参照してください)。 (新しいウィンドウを開きます))。🚨

参考文献

手動(逆)シリアル化は、面倒でエラーが発生しやすいプロセスです。borshcrateをチェックしてください (新しいウィンドウを開きます)これはあなたのためにこれを自動化することができます。

アンカー (新しいウィンドウを開きます)、solanaプログラムを作成するためのフレームワークは、さらに進んでプログラミングプロセス全体を簡素化します。

編集と確認

  • 2021/01/18:バグ修正に関するセクションを追加
  • 2021/01/31:「nonce」の名前を「bumpseed」に変更
  • 2021/02/08:solletは、base58でエンコードされた秘密鍵の代わりにバイト配列をエクスポートするようになりました
  • 2021/03/19:トークンアカウントのリンクを更新
  • 2021/05/02:読みやすさを改善し、solletバイト配列を取得する方法を説明します
  • 2021/05/02:「set_token_authority」を「set_authority」に変更
  • 2021/05/11:process_exchangeの後に欠落しているデータのゼロ化を追加
  • 2021/05/19:依存関係を更新し、トークンプログラムチェック警告を追加
  • 2021/05/29:アカウントとして渡されることなくsysvarsにアクセスできるようになりました
  • 2021/08/11:フロントランニングクイズを追加し、フローを改善しました
  • 2021年8月30日:キャンセル実装に関する潜在的な改善セクションに追加の警告-のおかげピエールアロワナ (新しいウィンドウを開きます)ウィリアムアーノルド(新しいウィンドウを開きます)
  • 2021年9月27日:キャンセル改善警告、追加のスクリプト、追加borshさらに読書へのアンカー、および固定壊れたリンク-のおかげでトニー・リッチアーディ (新しいウィンドウを開きます)リンクを見つけるため
  • 2021/10/26:非推奨のコミットメントレベルを削除し(不和からSundeep Charan Ramkumar#2703に感謝)、データとランプのAccountInfoヘルパーを追加しました
  • 2021/10/28:簡略化されたトークンアカウント図
  • 2021/11/19:XとYが何であるかを明確にするためにトークンのアナロジーを追加しました(Twitterの@albttxに感謝します)
ABOUT ME
たけ
はじめまして! たけといいます。 20代男性サラリーマンが資産運用で5年で3000万をめざします。 これを読んで自分でも出来るのではないかと思ってくれる人が増えると嬉しいです。 お金を得ることは手段に過ぎません。若いうちに稼いで、自分の時間をより大切なことに使いたいです。 【2019投資戦歴】 投資資金合計 300万 2019年度単年損益(年利) FX 15万(15%) 投信 9万(7%) 株式 4万(8%) ※投信、株式は含み益