見出し画像

SolanaでHelloWorldしてみる

Solana Hacker House Tokyoでの成果

素人すぎてチーム組んでHackersonなんてとてもとてもできないので、モクモクとチュートリアルにこの5日間かけて挑んでみた。とりあえず、Helloとログに出力するスマートコントラクト?と呼び出すクライアントを(半分写経だけど)、つくることができた。
以下記録メモです。

日々の成果:

1日目(5月25日 水曜日):
最高の環境と最高のコミュニティに参加できたことは理解できたのだけれども、自分のレベルが低すぎてさっぱりなにやっていいか途方にくれる。夕方のHandsOnプレゼンをみて、よしこれを5日かけてやってみようと思った。

夜:結局店から出て行ってくれと言われるまで飲む。6時から10時までの4時間耐久宴会となった。私は基本引きこもりタイプなのではあるが、酒の勢いを借りていろいろ声をかけてみると、STEPNだったり海外移住考えているというような会話になる。はっきりいってみんな異常(いい意味でのクレージー)である。ちょー楽しい。最終日の宴会も参加したい。

2日目(5月26日 木曜日):
  ちょいと別件あり自宅で作業。
  環境の整備まで完了
   1) SolanaSDKのインストール
   2)Rust, NodeJSのインストール
   3)ローカルバリデータの起動完了

https://github.com/solana-labs/example-helloworld/blob/master/README.md

ここのhello worldがとてもよくできていて、というかできすぎていて、ローカル環境にデプロイできてしまったのだが、なにやっているかさっぱりわからん。3日目はこのあたりをさぐりつつ、MyHelloWorldをつくっていってみよう。

3日目(5月27日 金曜日):
雨の中現地10:00到着。新しいストラップをもらって場所確保。もくもく開始。
今日理解したことは、Rustで作るターゲットはエントリーポイントをExportしたダイナミックリンクライブラリ(*.soファイル)。SDKで拡張された cargo build-bpfコマンドでビルド可能。それにあわせて Cargo.tomlなど修正していく。
結果、サーバ側のデプロイまではできたけれど、クライアント作ってアクセスしないとハローワールドされない。

4日目(5月28日 土曜日):

9:30に到着。今日は、帽子もらった!!
本日の実績:
・サーバ側にデプロイしたHelloWolrdスマコンを呼び出すクライアントを作った。
  1) TypeScriptでクライアント作った。
  2) ハードコーディングして、クラスタのデプロイしたプログラムを呼び出すことができた。
     STEP1) クラスタへの接続
     STEP2)トランザクション呼び出し

わかったことは、デプロイしたプログラムのエントリーポイントは型がきまっていて、また*.soファイル1つに1つしかない。その呼び出しパラメータに、アカウント情報がはいっているので、それをつかって、スマートコントラクトを実装する仕組らしい。
なお、エンドポイントの引数の型は、[&u8]らしく、事実上なんでもあり。クライアントとサーバ側でパラメータ決めてをシリアライズする仕組みたい。
すごくシンプルでいいけど、かなりプリミティブな機能でみんなどうやって複雑なアプリつくっているのだろう。DAY5でDeepDiveしてみよう。


5日目:(5月29日 日曜日)

9:20到着。早く来すぎてしまって会場一番乗り。

今日目覚めて4日分の成果が理解できてきた。誤解しているかもだけど、もしかしてSolanaって「サーバレス+履歴付きのCORBA」かしら。
そう思った理由:
 スマコンのエンドポイントって実は、引数1つでバイナリ。
 パラメータをシリアライズ・デシリアライズするのはSDKの役割。
 Solanaはクライアントから来たメッセージをバイナリにメッセージングをしている役割みたい。

とすると今日DeepDiveすべきところは以下かなあ。
 1)クライアントからのメッセージルーティング
 2)履歴の作り方・クエリ方法
 3)アカウント
     

5日でHelloWorldをやってみる。

SolanaどころかEtherでも未経験なスマートコントラクトを実装してみよう。具体的には、Cooking with Solanaを見ながらHacker Houseで初日にプレゼンされたスマートコントラクトによるカウントアップ・ダウンをやってみよう。

ハローワールドだったら以下のサンプルのほうが楽だったかも。とりあえずだまってこれに従えば、デプロイまでのワークフローが理解できそうだ。


開発環境の整備

  • nodeJS (最後までコマンドでいけばいらないかも)

  • Rust

  • Solana CLI

スマートコントラクトは、Rustで記述するらしい。Rustも未経験なので、なんとも言えないけれど、go-langのようにコンパイルすると各環境のネイティブバイナリが出てきそうなイメージをもっていた。どうもSolnaのVM上で動くということなのだろう(追記:https://llvm.org LLVM上でうごくんだって)。

NodeJSのインストール

できたら、新しく覚える言語はRustだけにしたいけれど、Node(つーかJavaScript)もWeb連携するときにいるようなので、以下のページをみて、自分の環境にあわせてインストールする。

Macの場合は、HomebrewでインストールでもOK.

brew install node

Rustのインストール

Rustのインストールページは以下。

Solna SDKのインストール 

インストールコマンド

sh -c "$(curl -sSfL https://release.solana.com/v1.10.19/install)"

再ログインするとパスが有効になる。(.profileに書かれているので、source .profileでもOK)

バージョンの確認

% solana --version
solana-cli 1.10.19 (src:5e56677d; feat:2368599632)

ローカルホスト(❌Devネット)(❌テストネット)へ接続するように設定をする

チェーンはどうもメインネットの他にメインネット、テストネット、Devネットがあるようだ(さらにローカルにインストールしてあればLocalネット)。
通常の開発ではローカルもしくはDevnetを使うということらしいのでDevネットを最初指定してみたのだが、GAS代の管理が大変、他人のログも出てわかりにくいなど初心者には難しかった。
結果、まずはローカルホストを利用することにした。

$ solana config set --url localhost

この設定は、$(HOME)/.config/solana/cli/config.yml に書き込まれる。たとえば以下のようになる。

$ more config.yml 
---
json_rpc_url: "http://localhost:8899"
websocket_url: ""
keypair_path: /Users/yasstake/.config/solana/id.json
address_labels:
  "11111111111111111111111111111111": System Program
commitment: confirmed

テストバリデーターの起動

2つターミナルを起動する。一つでローカルホストにテストバリデーターを起動
し、もうひとつでログを表示させる。

$ solana-test-validator

ログの表示

$ solana logs

テストバリデータのバージョン確認(ローカルホストへの接続確認)

$ solana cluster-version
1.10.8

残高の確認

$ solana balance
499999999.322960496 SOL

ちょっとつかちゃったので違う値がでるけれど、ディフォルトは500000000SOLが入っているようだ。

solana explorerの利用

ローカルホストであっても上記Explorerはチェーンの状態を検索できる。そもそもSolanaにどういう情報がはっているのかわからない初心者は、絶対使ったほうがいい(私ももっと早くきがつけば良かった)

DAY3(5月27日金曜日)作業

まずはSolanaとはなれて、生RustでHelloWorld

今回プログラム名をhello としたいので以下のコマンドでビルドでディレクトリをつくる。--binでバイナリを指定しているが、Solanaでデプロイするプログラムはダイナミックリンクライブラリなので、そのときは--libを指定する。ただ、違いは雛形ファイルとしてmain.rsが出力されるか、lib.rsが出力されるかの違いなのであとで変更すればいい(ファイルのリネーム)。

$ cargo new hello --bin
Created binary (application) `hello` package

作られたプロジェクトフォルダを確認する。

$ tree hello/
hello/
├── Cargo.toml
└── src
    └── main.rs

Cargo.tomlはマニュフェストになっていてプログラムのバージョンとか依存関係のライブラリとか記載するようだ。main.rsがメインファイル。開くとなんとすでにハローワールドが入っていた。

fn main() {
 println!("Hello, world!");
}

!って否定演算子かとおもったけど、ぐぐったらマクロの意味なんだって。

通常版HelloWorld実行

helloディレクトリにはいって、cargo runでビルド〜実行までやってくれる。

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/hello`
Hello, world!

あっさり動いた。

Solanaに向けての作戦

  1. マニュフェストへライブラリのリンク・出力形式など追加(Cargo.toml)

  2. Solana版ハローワールド作成(ところでどこに出力されるのだ??)

  3. ダイナミックリンクライブラリの作成(cargo build-bpf)

マニュフェストへのライブラリのリンク・出力形式の指定

Solana用のライブラリを使う指定

https://docs.rs/solana-program/1.10.19/solana_program/

Cargo.tomlに、エントリーポイントなし、 solana-program="1.10.19"を利用、ダイナミックリンクライブラリの出力指定の3点をを追加すればいいらしい。

[features]
no-entrypoint = []          # エントリーポイントなし?

[dependencies]
solana-program="1.10.19"    # Solana SDKの利用宣言とバージョン指定

[lib]
name='hello'
crate-type = ["cdylib", "rlib"]  # cdylib:ダイナミックライブラリ用ビルド

クリエートじゃなくて、クレート(crate)なのがハマりポイント。クレートはコンパイルしたときの出力単位みたい。cdylibが他システムから呼ばれるためのダイナミックライブラリを作る時指定のよう。じゃあrlibはなに?というところがいまいちわからないけれど、lib.rsファイル1つだけだとあまり関係ないけど、ファイルを分割していくとはまるらしいので指定。

Solana版ハローワールド作成(ところでmsg!は、どこに出力されるのだ??)

ライブラリを出力するときのRustのメインソースファイルはmain.rsではなくて、lib.rsではなくてはいけないらしいので、lib.rsにmain.rsをリネームする。

中身はほぼ、https://docs.rs/solana-program/1.10.19/solana_program/macro.entrypoint.htmlの丸パクリだけど、利用していない引数について_(アンダースコア)をつけてエラーを抑止する。
msg!がSolana版のprintらしい。

 (https://docs.rs/solana-program/1.5.4/solana_program/macro.msg.html)


#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint {

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

    entrypoint!(process_instruction);

    pub fn process_instruction(
        _program_id: &Pubkey,
        _accounts: &[AccountInfo],
        _instruction_data: &[u8],
    ) -> ProgramResult {
        msg!("Hello world");

        Ok(())
    }
}

entrypoint! は、Solana SDKのマクロ。1つの*.soに1つしか定義できないみたい。なるほど、1つのプログラムIDだけで呼びだせる理由がわかった。
https://docs.rs/solana-program/1.5.4/solana_program/entrypoint/index.html

cargo build-bpfコマンドでビルドする。

$ cargo build-bpf
BPF SDK: /Users/yasstake/.local/share/solana/install/releases/1.10.8/solana-release/bin/sdk/bpf
cargo-build-bpf child: rustup toolchain list -v
cargo-build-bpf child: cargo +bpf build --target bpfel-unknown-unknown --release
    Finished release [optimized] target(s) in 0.29s
cargo-build-bpf child: /Users/yasstake/.local/share/solana/install/releases/1.10.8/solana-release/bin/sdk/bpf/dependencies/bpf-tools/llvm/bin/llvm-readelf --dyn-symbols /Users/yasstake/Projects/hello/target/deploy/hello.so

To deploy this program:
  $ solana program deploy /Users/yasstake/Projects/hello/target/deploy/hello.so
The program address will default to this keypair (override with --program-id):
  /Users/yasstake/Projects/hello/target/deploy/hello-keypair.json

hello.soができるだけではなく、親切にもデプロイ方法まで教えてくれる。hello-keypair.jsonもしらない間に作られていた。build-pbf引数はSolana SDKが拡張してくれているようだ。

プログラムをローカル環境へデプロイする

$solana program deploy /Users/takeo/Projects/solana/hello/server/hello/target/deploy/hello.so
The program address will default to this keypair (override with --program-id):
  /Users/takeo/Projects/solana/hello/server/hello/target/deploy/hello-keypair.json
(base) TAKEOnoMacBook-Pro:hello takeo$ solana program deploy /Users/takeo/Projects/solana/hello/server/hello/target/deploy/hello.so
Program Id: 4Wok24RdDSNfmYyvM1RNwhf66CTS4JLiEJkNPbSup8UC

!! 祝 !! デプロイまで完了。とはいえ、やっぱりJavaScriptでアクセスしないとハローワールドされないのかあ。。。。

浮かれてしまったが、ここで表示されるプログラムIDがチェーンの中でのプログラムIDなのであとで呼び出すときに必要。出力のとおり、hello-keypair.jsonは秘密鍵が保存されている。

DAY4(5月28日土曜日) クライアントの作成

JSON RPC APIドキュメントは以下。

さすがに直接叩くのは大変なのでJavaScriptのラッパーを使う。マニュアルは以下。

クラスタへの接続

コテコテのハードコーディングでローカルバリデータへ接続するには以下のようにurlをいれてConnectionオブジェクトを生成する。第二引数は、Confirmationレベルで、接続先ノードだけで承認された"processed", クラスターで1承認された"Confirmed", 最終承認までされた”finalized"があるようだ(https://solana-labs.github.io/solana-web3.js/modules.html#Commitment

    const url = 'http://127.0.0.1:8899'
    connection = new Connection(url, 'confirmed')

乱暴だけどいきなりhello呼び出してみよう。

TransactionInstruction ( https://solana-labs.github.io/solana-web3.js/classes/TransactionInstruction.html )を組み立ててConnectionパラメータ入れて、sendAndConfirmTransactionhttps://solana-labs.github.io/solana-web3.js/modules.html#sendAndConfirmTransaction ),GAS代をはらうpayerを指定してをよびだせばいいみたい。
実際のところ、この引数はSDKでバイナリにシリアライズされて送信され、受けた側でデシリアライズするみたい。Cのサンプルプログラムみると、エントリーポイントの定義は、extern uint64_t entrypoint(const uint8_t *input) こんなかんじ。なんとシンプル。

  const instruction = new TransactionInstruction({
    keys: [{pubkey: greetedPubkey, isSigner: false, isWritable: true}],
    programId,
    data: Buffer.alloc(0),
  });

  await sendAndConfirmTransaction(
    connection,
    new Transaction().add(instruction),
    [payer],
  );

出力は、Hello Worldってクラスタのログに出るんだった(3日かけたのに結果が地味で悲しい)

Transaction executed in slot 57566:
  Signature: 4vHvqE9HqQ171ucXnzWtAS8sBHns57Hrda2sz3nLqUQ9HD9mWKTxRHShrxKShTRK78YN9vmSGL6mVwt1UL1zecVo
  Status: Ok
  Log Messages:
    Program 4Wok24RdDSNfmYyvM1RNwhf66CTS4JLiEJkNPbSup8UC invoke [1]
    Program log: Hello world
    Program 4Wok24RdDSNfmYyvM1RNwhf66CTS4JLiEJkNPbSup8UC consumed 417 of 1400000 compute units
    Program 4Wok24RdDSNfmYyvM1RNwhf66CTS4JLiEJkNPbSup8UC success


要調査: DLLのエンドポイントはどう指定するのだろう?(1DLLに1つしかエントリーポイントがないが答えだった)


今日はここまで。
ローカル環境のFEEがたりなくなってデプロイできなくなった。AIRドロップもいまいちうごかず。明日DeepDiveするか。

アカウントへの接続とトランザクションFeeの確認

ステートを保存するためのエリアはバイト単位で課金されるようだ(あるいは2年分の費用をステーキングすると費用免除みたい。ある意味年利50%!!)

今回、まずステートレスで始めるつもりだったけど、サンプルプログラムが呼び出し回数を保存しているので、それに倣うと何かが要りそう。


DAY5(5月29日 日曜日) DeepDive

やってみようとおもうこと

1)クライアントからのメッセージ・ルーティング調査
2)アカウントのしくみ調査

かなあ。

デプロイしたプログラムの状況確認

APIサーバのLookup

DIGでみるとこんな感じ。

$ dig api.mainnet-beta.solana.com.
;; ANSWER SECTION:
api.mainnet-beta.solana.com. 30	IN	A	147.75.92.237
なんかHackerHouseTokyo会場隣のEqunix TYにありそうなIPがかえってきた。

TTL30秒なのでラウンドロビンしていて、もういちど叩くと 147.75.42.215@香港が帰ってきた。

(クライアント)→(APIサーバ:ラウドロビン)→(クラスタ)
という流れでリクエストがいくみたいだ。





IPアドレス147.75.92.237ホスト名tyo12.rpcpool.com国

Japan地域Tokyo郵便番号135-0051


$ dig api.mainnet-beta.solana.com.

; <<>> DiG 9.10.6 <<>> api.mainnet-beta.solana.com.
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30455
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;api.mainnet-beta.solana.com. IN A

;; ANSWER SECTION:




((ーーーただいま作業中ーーーーー))









デプロイしたプログラムへアクセスするクライアントの作成

TSファイルを直接実行できる ts-nodeをインストールする。(えいやっとグローバルスコープで)

$ npm install -g ts-node

tsconfig.json ファイルを作る。

$ tsc --init

Created a new tsconfig.json with:                                                           
                                                                                         TS 
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true


You can learn more at https://aka.ms/tsconfig

ライブラリインストール

Web3.js  spl-token  Wallet-Adapter をインストールする。

$ npm install --save @solana/web3.js
$ npm install --save @solana/spl-token
$ npm install --save @solana/wallet-adapter-wallets @solana/wallet-adapter-base

ここにきて、JavaScriptを知らないのが辛くなってきたぞ。
ライブラリのインストールは、package.jsonのDependencyに書いてnpm update したほうが簡単らしい。

だんだん、公式Example Hello Worldの写経タイムになってきてしまった。

(-----作業中ーーーー>




$ tsc --init


Created a new tsconfig.json with:                                                           

                                                                                         TS

  target: es2016

  module: commonjs

  strict: true

  esModuleInterop: true

  skipLibCheck: true

  forceConsistentCasingInFileNames: true



You can learn more at https://aka.ms/tsconfig





((((作

業ここまで))))




上記URLの丸コピーだけれども、HelloWorldがあったのでこれを通常版HelloWorldに追加する。

#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint {

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

    entrypoint!(process_instruction);

    pub fn process_instruction(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        instruction_data: &[u8],
    ) -> ProgramResult {
        msg!("Hello world");

        Ok(())
    }

}

マニュフェストの方には、mainが不要なので、no-entry-pointを入れる。

ファイル名を lib.rsへリネームする。





TODO:
  Rustちょっと覚える(そこからはじめていいのかっ!!)
  ひきつづきMyHelloWorldをつくってみることにチャレンジする。

Rustはパッと見た目以下の「Rust入門」がまとまっていて良さそう。

Rust向けの開発環境
・とりあえずEmacsでいくか

Rustのパッケージング
・Cargoでつくったあとのディレクトリ構成の理解


Solanaでの最低限の要求
・ビルドした結果とエントリーポイント




ーーーーーーーーーーーーーーーーー

ウォレットを作る

一番簡単そうなFile System Walletをつくってみる。

ウォレット保存用のディレクトリを作る。

$ mkdir ~/solana-wallet

作成したウォレット用のディレクトリに秘密鍵を作る。

$ solana-keygen new --outfile ~/solana-wallet/keypair.json

途中暗号化するか聞かれるが、無視してリターンすると暗号化されない秘密鍵が作成される。ついでに以下のとおり公開鍵も出力される。

BIP39 Passphrase (empty for none):Wrote new keypair to /Users/yasstake/solana-wallet/keypair.json
========================================================
pubkey: HTaKPkBiHHrBuEDKPNMCWM5yrmrWE6K337AZp6BLWEDf
========================================================

Publicキーを忘れてしまっても、あとから以下のコマンドで再表示可能。

$ solana-keygen pubkey ~/solana-wallet/keypair.json
HTaKPkBiHHrBuEDKPNMCWM5yrmrWE6K337AZp6BLWEDf

solana balanceコマンドで残高を確認してみよう。当然0が表示される。

$ solana balance HTaKPkBiHHrBuEDKPNMCWM5yrmrWE6K337AZp6BLWEDf
0 SOL


アプリを作る

Rustでオンチェーンプログラムつくるときのマニュアルは以下。

大きな流れとしては以下のとおり

Rustの標準的なディレクトリ構成を作る。
Cargo.tomlを編集する(no-entrypointを設定する)

Rustで新しいプログラムを作る。バイナリープログラムを作るので$ cargo new <program名> --bin で作る。


こっちをやったほうが早かったか?






(ーーーーここから先はまだ計画中ーーーーーー)

ステータスを管理する領域とプログラムを管理する領域それぞれにアカウントがいるっぽい。データアカウント/プログラムアカウントを作る。



カウントアッププログラムを作る


デプロイする



動かしてみる

用語集

調べた過程で出会った未知の用語メモ。

lamports: SOLの最小単位。BTCでいうとこのSATOSHIにあたるものみたい。


過去いろいろやった形跡

solana config set --url https://api.devnet.solana.com

動作確認

Devネットクラスタのバージョン確認

% solana cluster-version
1.9.21

DevネットからエアードロップでSOLをもらう

スマートコントラクトのメモリエリアを確保したり、スマートコントラクトを動かすにはSOLが必要。Devネット用には、無料でSOLがもらえるのでまず受け取る。
$ solana airdrop <ほしいSOLANA数> <公開鍵> --url <Devネットのアドレス>

$ solana airdrop 1 HTaKPkBiHHrBuEDKPNMCWM5yrmrWE6K337AZp6BLWEDf --url https://api.devnet.solana.com
Requesting airdrop of 1 SOL
Signature: 2X1gyFEor8sRxgFKS7BMc4k2LAGxoHY1pz3Riud45hyGzfsZJxksmxCUEm5VDWyPpRTZYKALB4rdcm4ZXiodCKC3
1 SOL

なお、いきなり10SOL要求したらエラーになった。一方、同じコマンドもう一度叩いたら、もう1SOLがもらえた(合計2SOL)
2回コマンド叩いたあと、ウォレットの残高を確認してみる。

$ solana balance HTaKPkBiHHrBuEDKPNMCWM5yrmrWE6K337AZp6BLWEDf
2 SOL

どこまでもらえるか試すのが目的じゃないので、次に進む。

その他

npm updateで関連するDependency をupdate / install する


PrimiseクラスはJavaScriptのクラスだった。 returnObject.then(), returnObject.catch()という処理ができるみたい。

スロットてなによ?



この記事が気に入ったらサポートをしてみませんか?