https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/
Solanaでのプログラミング-はじめに
#イントロとモチベーション
このガイドは、Solanaでのコーディングの概要を説明することを目的としています。 (新しいウィンドウを開きます)例としてエスクロープログラムを使用したブロックチェーン。一緒にコードを調べ、エスクロープログラムを段階的に構築します。プログラムを試すために使用できるUIも作成しました。さらに、(恥知らずなプラグ)spl-token-uiで遊ぶことができます (新しいウィンドウを開きます)。
このブログ投稿のほとんどの情報は、ドキュメントまたはサンプルプログラムのどこかにあります。そうは言っても、コーディング理論の大部分を段階的に説明し、実際に適用するガイドは見つかりませんでした。この投稿がこれを達成し、ソラナプログラムの理論と実践を織り交ぜることを願っています。ソラナの予備知識は必要ありません。これはRustチュートリアルではありませんが、Rustドキュメントにリンクします (新しいウィンドウを開きます)新しいコンセプトを紹介するときはいつでも。また、関連するSolanaのドキュメントにリンクしますが、フォローするためにそれらを読む必要はありません。
重要な理論は次のように投稿に振りかけられます:
ソラナでは、スマートコントラクトはプログラムと呼ばれます
そして、各セクションの終わりに次のように要約されています。
-
- ソラナでは、スマートコントラクトはプログラムと呼ばれます
私はすべてのトピックを説明するとは言いませんが、これが読者がソラナをさらに探求するための確かな出発点になることを願っています。SolanaとRustを初めて使用し、この投稿を途切れることなく終了し、説明されているすべての概念と言及されているリンクをしっかりと理解したままにしておきたい場合は、1日を投稿に割り当てることをお勧めします。
何かが機能しておらず、その理由がわからない場合は、ここで最終的なコードを確認してください (新しいウィンドウを開きます)。
間違いを見つけたり、フィードバックを送りたい場合は、discord paulx#9059で私に連絡してください。 (新しいウィンドウを開きます)またはツイッター (新しいウィンドウを開きます)。
#最終製品
コーディングを始める前に、私たちが構築しているものを理解するために最終製品であるエスクロープログラムを見てみましょう。
#エスクローとは何ですか?
エスクロースマートコントラクトは、ブロックチェーンが可能にすることを十分に強調しながら、理解しやすく、コード自体に集中できるため、見て構築するのに適した例です。コンセプトに不慣れな方のために、ここに簡単な説明があります。
アリスがアセットAを持ち、ボブがアセットBを持っていると想像してください。彼らは自分の資産を取引したいと思っていますが、どちらも最初に自分の資産を送りたくありません。結局のところ、相手方が取引の終了を延期せず、両方の資産で逃げ出した場合はどうなるでしょうか。誰も最初に資産を送りたくない場合、デッドロックに達します。
この問題を解決する従来の方法は、AとBの両方が信頼するサードパーティCを導入することです。これで、AまたはBが最初に移動し、アセットをCに送信できます。Cはその後、自分の資産を送信するだけにしない、相手を待ちCは両方の資産をリリース。
ブロックチェーンの方法は、信頼できるサードパーティCをブロックチェーン上のコードに置き換えることです。具体的には、信頼できるサードパーティと同じように検証可能に機能するスマートコントラクトです。スマートコントラクトは、信頼できるサードパーティよりも優れています。たとえば、信頼できるサードパーティが取引の相手と共謀していないことを確認できますか?コードを実行する前にコードを確認できるため、スマートコントラクトを確認できます。
ここでこの背景セクションを終了します。インターネットには、ブロックチェーンのエスクローに関する多くの資料がすでにあります。それでは、ソラナにそのようなエスクローを構築する方法を見てみましょう。
#エスクロープログラムの構築-アリスのトランザクション
#プロジェクトの設定
頭の上のテンプレートレポ (新しいウィンドウを開きます)、をクリックUse this template
して、リポジトリを設定します。ソラナのエコシステムはまだ若いので、これが今のところ私たちが持っているものです。Rust拡張機能を備えたVscodeが私が使用しているものです。あなたも必要になりますRust
(新しいウィンドウを開きます)。さらに、ここに行きます (新しいウィンドウを開きます)Solana開発ツールをインストールします。(Macを使用していて、必要なバージョンのバイナリがない場合は、「ソースからビルド」セクションに従って、インストールされているビンをパスに追加します。このsolana-install init
手順は不要であり、機能しません。無視してください。コマンドが見つからないためビルドします。coreutilsをインストールしてみてください (新しいウィンドウを開きます)およびbinutils (新しいウィンドウを開きます)自作で)。
solanaプログラムをテストする方法がまだわからない場合は、すべてのテストコードを削除してください。プログラムのテストは、別のブログ投稿のトピックです。の横lib.rs
のtests
フォルダだけでなく、のテストコードも削除します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"]
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;
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
2
3
4
5
6
7
8
9
10
11
12
この構造を使用したプログラムのフローは次のようになります。
- 誰かがエントリポイントを呼び出します
- エントリポイントは引数をプロセッサに転送します
- プロセッサは、エントリポイント関数から引数
instruction.rs
をデコードするように要求しinstruction_data
ます。 - デコードされたデータを使用して、プロセッサは要求の処理に使用する処理関数を決定します。
- プロセッサは
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つ問題があります。アリスは正確に何に所有権を譲渡しますか?プログラムから派生したアドレスを入力します (新しいウィンドウを開きます)。
-
- 開発者は、
data
フィールドを使用してアカウント内にデータを保存する必要があります - トークンプログラムは、
data
フィールド内に関連情報を保持するトークンアカウントを所有しています(新しいウィンドウを開きます) - トークンプログラムは、関連データを含むトークンミントアカウントも所有しています(新しいウィンドウを開きます)
- 各トークンアカウントは、トークンミントアカウントへの参照を保持しているため、どのトークンミントに属しているかが示されます。
- トークンプログラムにより、トークンアカウントの(ユーザースペース)所有者は、その所有権を別のアドレスに譲渡できます。
- Solanaの内部アカウント情報はすべて、アカウントのフィールドに保存されます (新しいウィンドウを開きます)ただし、ユーザースペース情報のみを目的としたデータフィールドには決して入りません
- 開発者は、
#プログラム派生アドレス(PDA)パート1
エスクローが開いていてボブのトランザクションを待っている間に、プログラムがXトークンを所有する方法が必要です。問題は、プログラムにトークンアカウントのユーザースペースの所有権を与えることができるかどうかです。
秘訣は、トークンアカウントの所有権をエスクロープログラムのプログラム派生アドレス(PDA)に割り当てることです。今のところ、このアドレスが存在することを知っていれば十分であり、プログラムにトランザクションに署名させたり、アカウントのユーザースペース所有権を割り当てたりするために使用できます。PDAについては後で詳しく説明しますが、とりあえずコーディングに戻りましょう。
#Instruction.rsパート2
instruction.rs
このファイルがプログラムのAPIを定義することを知っていましたが、まだコードを記述していませんでした。InitEscrow
APIエンドポイントを追加してコーディングを始めましょう。
// 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
}
}
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
一時アカウントの所有権を譲渡するにはアリスの署名が必要なため、署名者としてアカウント0、具体的にはアカウント0が必要です。コードでは、アリスをイニシャライザー、ボブをテイカーと呼びます(アリスはエスクローを開始し、ボブは取引を行います。より良い名前を思い付くことができるかどうか教えてください)
1. `[writable]` Temporary token account that should be created prior to this instruction and owned by the initializer
アカウント1は、書き込み可能である必要がある一時トークンXアカウントです。これは、トークンアカウントの所有権の変更はユーザースペースの変更でdata
あり、アカウントのフィールドが変更されることを意味するためです。
2. `[]` The initializer's token account for the token they will receive should the trade go through
アカウント2はアリスのトークンYアカウントです。最終的には書き込まれますが、このトランザクションでは発生しません。そのため、角かっこを空のままにしておくことができます(読み取り専用を意味します)。
3. `[writable]` The escrow account, it will hold all necessary info about the trade.
アカウント3はエスクローアカウントであり、プログラムがエスクロー情報を書き込むため、書き込み可能である必要があります。
4. `[]` The rent sysvar
アカウント4はRent
sysvarです。processor
コードを書き始めたら、これについて詳しく説明します。
5. `[]` The token program
今のところ覚えておくべきことは、アカウント5はトークンプログラム自体のアカウントであるということです。processor
コードを書くときに、なぜこのアカウントが必要なのかを説明します。
Solanaには、使用しているSolanaクラスターのパラメーターであるsysvarがあります。これらのsysvarは、アカウントを介してアクセスし、現在の料金や家賃などのパラメーターを保存できます。
solana-program
バージョンの時点で1.6.5
、sysvarsは、アカウントとしてエントリポイントに渡されることなくアクセスすることもできます (新しいウィンドウを開きます)(このチュートリアルでは、今のところ古い方法を引き続き使用しますが、使用しないでください!)。
InitEscrow {
/// The amount party A expects to receive of token Y
amount: u64
}
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)
}
}
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。その命令をビルドして返します。
未定義のエラーを使用しているため、これはコンパイルされません。次にそのエラーを追加しましょう。
-
- Solanaには、使用しているSolanaクラスターのパラメーターであるsysvarがあります。これらのsysvarは、アカウントを介してアクセスし、現在の料金や家賃などのパラメーターを保存できます。
solana-program
バージョンの時点で1.6.5
、sysvarsは、アカウントとしてエントリポイントに渡されることなくアクセスすることもできます (新しいウィンドウを開きます)(このチュートリアルでは、今のところ古い方法を引き続き使用しますが、使用しないでください!)。
- Solanaには、使用しているSolanaクラスターのパラメーターであるsysvarがあります。これらのsysvarは、アカウントを介してアクセスし、現在の料金や家賃などのパラメーターを保存できます。
#error.rs
error.rs
他のファイルの隣に新しいファイルを作成し、内部に登録しますlib.rs
。次に、次の依存関係をCargo.toml
...
[dependencies]
solana-program = "1.6.9"
thiserror = "1.0.24"
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,
}
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)
}
}
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)
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
何が起こっているのかを開梱しましょう。まず、instruction_data
fromentrypoint.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(())
}
}
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(())
}
...
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_account
とPDA。temp_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"]}
2
3
4
ここでは、他の依存関係とは少し異なる方法で依存関係をインポートしています。これは、独自のエントリポイントを持つ別のSolanaプログラムをインポートしているためです。ただし、プログラムには、前に定義したエントリポイントを1つだけ含める必要があります。幸いなことに、トークンプログラムは、貨物機能の助けを借りてエントリポイントをオフにするスイッチを提供します (新しいウィンドウを開きます)。他の人が私たちのプログラムをインポートできるように、私たちのプログラムでもこの機能を定義する必要があります!ヒントをいくつか残しておきます。トークンプログラムをチェックしてください。 (新しいウィンドウを開きます)Cargo.toml
とそのlib.rs
。自分で理解できない、または理解したくない場合は、私が作成したエスクロープログラムを調べることができます。
に戻りprocessor.rs
ます。solana_program
useステートメントをコピーして置き換え、次のコードを追加します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(())
...
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;
に
use crate::{instruction::EscrowInstruction, error::EscrowError};
もう一つのなじみのないことがここで起こっています。初めて、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,
}
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_pubkey
とis_initialized
。後者については今説明し、前者については後で説明します。
is_initialized
特定のエスクローアカウントがすでに使用されているかどうかを判断するために使用します。これ、シリアル化、および逆シリアル化はすべて、特性で標準化されています (新しいウィンドウを開きます)program pack
モジュール (新しいウィンドウを開きます)。まず、とを実装Sealed
しIsInitialized
ます。
// 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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
Sealed
Sized
両者の間に違いはないようですが、これはソラナのバージョンのルストの特徴です。さて、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),
})
}
}
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) = 105
。u8
コーディングが簡単になり、余分な無駄なビットのコストがごくわずかであるため、ブール値に全体を使用しても問題ありません。
エスクローの長さを定義した後unpack_from_slice
、の配列をu8
上記で定義したエスクロー構造体のインスタンスに変換する実装を行います。ここではあまり興味深いことは起こりません。ここで注目すべきは、arrayrefの使用です。 (新しいウィンドウを開きます)、スライスのセクションへの参照を取得するためのライブラリ。ドキュメントは、ライブラリの(わずか4つの)異なる関数を理解するのに十分なはずです。必ずライブラリをに追加してくださいCargo.toml
。
...
[dependencies]
...
arrayref = "0.3.6"
...
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();
}
}
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.rs
とunpack_unchecked
、定義していない関数を呼び出します。それはどこから来ているのでしょうか。答えは、トレイトにはオーバーライドできるデフォルトの関数を含めることができますが、オーバーライドする必要はないということです。 ここを見て (新しいウィンドウを開きます)Pack
のデフォルト関数について調べます。
state.rs
行われ、のがに戻ってみようprocessor.rs
と、私たちの1に調整use
文を。
から
use crate::{instruction::EscrowInstruction, error::EscrowError};
に
use crate::{instruction::EscrowInstruction, error::EscrowError, state::Escrow};
#プロセッサパート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()?)?;
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);
2
3
4
5
シードの配列とを関数に渡すことにより、PDAを作成program_id
しfind_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
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_program
useステートメントをコピーして置き換えます。続い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_authority
CPIにし、署名者のパブキーとして彼女のパブキーを含めることができることを意味します。トークンアカウントの所有者を変更するには、もちろん現在の所有者の承認が必要になるため、これが必要です。
命令の横に、呼び出しているプログラムのアカウントに加えて、命令に必要なアカウントも渡す必要があります。これらを調べるには、トークンプログラムに移動し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)
}
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
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
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
});
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);
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)))
})
2
3
4
5
6
7
8
9
10
11
12
ここではソラナライブラリからほとんど助けを得られないので、5番目で最後のix(エスクローを開始する場所)はより興味深いものです。コンストラクター(new TransactionInstruction...
)を呼び出して、手動で命令を作成します。必要なフォーマットはなじみ深いはずです!それはまさに私たちのプログラムエントリポイントが期待するものです。エスクロープログラムのprogramIdを渡し、次にキーを渡します。ここでは、特定のアカウントがtxに署名するかどうか(署名しない場合はtxが失敗するか)、アカウントが読み取り専用かどうかを指定します。その後、txに書き込まれる場合は失敗します。最後に、エントリポイントに到着するものをとして指定しますinstruction_data
。私たちは、で始まる0
最初のバイトは、私たちが使用したものであるため、instruction.rs
とtag
の命令をデコードする方法を決定します。0
を意味しInitEscrow
ます。次のバイトはexpected_amount
。bn.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'});
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,
}
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に含まれている必要があるのですか?自分で理解してみてください!
また、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()),
})
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)
}
}
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(())
}
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(())
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_signed
accounts引数のアドレスの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(())
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'});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ボーナス:バグ修正!
このプログラムにはバグがあります。それは重要なことではありません。Solanaでのプログラミングの微妙さを示しているので、そのままにしておきました。あなたはそれを見つけることができますか?
#Q&A
これは、ガイド自体が長すぎると思われる回答の読者から寄せられた質問のコレクションです。お気軽にご 連絡ください (新しいウィンドウを開きます)離れて聞いてください!
#アリスのXトークンの一時的なアカウントが本当に必要ですか?
Q:アリスのXトークンの一時的なアカウントは本当に必要ですか?アリスがエスクロー状態内で取引したいXトークンの量を節約し、ボブのトランザクション内で、エスクロープログラムにトークンプログラムへのCPIを作成させて、その量を彼女のアカウントから差し引くことはできませんか?
A:from
トークン転送のアドレスがトランザクションに署名する必要があるため、これは機能しません。ボブはアリスにトランザクションに署名するように依頼できますが、それではアリスとボブの間の通信がさらに必要になり、ユーザーエクスペリエンスが低下します。使用できるのはトークンアカウントの(ユーザースペース)delegate
プロパティですが、トークンアカウントは1つのデリゲートしか持てません。つまり、アリスは一度に1つの継続的なエスクローしか持てません。
#潜在的な改善
ユーザーエクスペリエンスを向上させるためのいくつかのアイデアがあります
- より良いUIを構築する
- 秘密鍵を公開せずにtxに署名する方法を追加します(例:Solongを使用) (新しいウィンドウを開きます)またはSOLウォレットアダプタ (新しいウィンドウを開きます))。
- きれいにする
- アドレスを指定してエスクローの状態を表示する機能を追加します
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プログラムを作成するためのフレームワークは、さらに進んでプログラミングプロセス全体を簡素化します。
- ドキュメント(新しいウィンドウを開きます)
- solana-program rustdocs(新しいウィンドウを開きます)
- ソラナミディアムアカウント(新しいウィンドウを開きます)
- トークンプログラム(新しいウィンドウを開きます)
- トークンプログラムのドキュメント(新しいウィンドウを開きます)
- システムプログラム(新しいウィンドウを開きます)
#編集と確認
- 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に感謝します)