Counter 白紙練習(第一弾) のつづきです。第一弾では、白紙から counter.compact を書いてコンパイル成功し、contract/output/contract/index.js が生成されるところまで進みました。
今回は 第二弾。その生成物を TypeScript の CLI 側から読み込み、ローカル Midnight に 実際にデプロイします。
このページは「最短の正解手順」ではなく、実験ノートです。実際にハマったエラー・確認した内容・直し方・「いまどこまで成功しているか」を、順番に残します。エラーが出ても大丈夫。エラーは、次に足りない部品を教えてくれる先生です。
今日の合言葉:
今回の到達点は、このうちここまでです。
1. 第二弾のゴール
今回のゴールは、第一弾で作った Counter 契約を、ローカル Midnight に実際にデプロイすること。まだ increment を呼んで状態を変えるところまでは行きません(それは第三弾)。
今回たどる道のり:
最後に見たいのは、この成功ログです(アドレスは例)。
このアドレスが「契約のチェーン上の住所」。第三弾でこれを使って呼び出します。
2. CLI ディレクトリを作る
counter-01 直下に cli/ を作ります。
CLI とは Command Line Interface(コマンドで操作する入口)のこと。ここでは「ターミナルから Midnight 契約を操作するためのスクリプト置き場」として使います。
3. 最初の deploy.ts
最初は、本当にただの動作確認でOKです。
実行:
最終的には、counter-01 直下から pnpm tsx cli/src/deploy.ts で動かす形に寄せます(理由は次の章)。
4. process.cwd() の罠
ここ、必ず覚えてください。process.cwd() は deploy.ts が置いてある場所ではありません。
counter-01 直下から実行するなら、process.cwd() は counter-01 を指します。だから、生成物への正しいパスはこう書きます。
間違いやすい例:
だから「実行する場所」を counter-01 直下に固定し、process.cwd() を基準にすると、パスがブレません。
5. index.js が見えるか確認する
いきなり import せず、まず「ファイルが見えているか」だけ確認します。
成功例:
まだデプロイではありません。でも、第一弾の output と第二弾の cli が、ちゃんと手をつなげたことの確認です。
6. 生成された index.js を import する
次に、本当に読み込めるか試します。await をいきなりトップレベルで使うと、こんなエラーが出ることがあります。
その場合は main() 関数で包みます(さっきの形と同じ理由)。
すると今度は、別のエラー:
これは壊れたのではありません。次に足りない部品を教えてくれているエラーです。生成された index.js の中身を見ると、こういう行があります。
しかも、ランタイムのバージョンまでチェックしています。
なので、counter-01 直下にそれを入れます。
第一弾の
contract-info.jsonに"runtime-version": "0.16.0"と書いてありましたね。あれが、ここで効いてきます。
7. Node.js の node_modules 探索のしくみ
なぜ cli/ ではなく counter-01 直下に入れたのか。ここも大事です。
今 import しているのは contract/output/contract/index.js。だから Node.js はこの近くから上へ探します。
counter-01/node_modules に依存が入っていれば、ちゃんと見つかります。一方、
は 兄弟ディレクトリなので、contract/... から上にさかのぼっても通り道に入らず、見つからないことがあります。
だから今回は、依存管理を counter-01 直下に一本化する方針にします。cli/package.json を作ってしまっていたら、最終的には消して counter-01/package.json にまとめるのが望ましいです。
8. Contract instance を作ってみる
依存を入れて再実行すると、import に成功します。
さらに、Contract のインスタンスを作って中身を覗きます。
成功例:
ここで初めて、TypeScript 側から Compact の生成物が見えました。まだデプロイではありませんが、「契約が部品になって、TS の世界に戻ってきた」決定的な瞬間です。
9. Docker の3点セットを確認する
ローカル Midnight でデプロイするには、3つの裏方が動いている必要があります。
確認:
成功例:
今回つなぐ先(endpoint):
⚠️ 古い endpoint を手書きで残さないこと。たとえば下のような「確認用メモ」を残したままにすると、後で混乱します。あとで削除し、以後は
network.tsが返すnetworkConfigを使います。
10. Midnight.js 依存を追加する
既存プロジェクト(kodomo01.app)の package.json を参考に、必要な依存を counter-01 直下に追加します。バージョンは固定して、噛み合わせを安定させます。
開発用:
インストール時に、こんな警告が出ました。
11. ESM / CommonJS 問題
wallet 系を読み込もうとして、こんなエラーが出ました。
見立て:
対策:
cli/package.json も残っているなら、そちらも一旦 ESM にします(ただし最終的には消して一本化)。
今後の初期セットは、最初からこうしておくと楽です。
12. network-id の使い方を確認する
最初、こう書きました。
でも NetworkId は実行時の値ではなく、型でした。モジュールの中身を実際に覗いて確認します。
結果:
正しい import はこちら。
README ではこう使うと書いてありました。
今回はローカルなので:
13. network.ts / wallet.ts / wallet-state.ts を移植する
既存プロジェクトの deploy.ts は、いくつかの補助ファイルに依存していました。
なので cli/src に移植します。最初は network.ts と wallet.ts だけコピーしたら、こうなりました。
足りない子を探して、連れてきます。
最終的にこの4つになりました。
14. network.ts が動くか確認する
部品を一気に全部つながず、1つずつ動かして確かめます。まずは network。
main() の上の方に:
成功ログ:
15. wallet を作れるか確認する
次は wallet。まだ同期(sync)まではせず、作れるかどうかだけ確認します。
Node.js には WebSocket のグローバルが無いので、ws で穴埋め(ポリフィル)します。
確認コード:
成功ログ:
この段階では、wallet を作れただけで、まだネットワークと同期はしていません。
16. wallet sync する
次に、wallet をネットワークに同期します。
成功ログ:
途中で、こんなログが出ることがあります。
17. DUST を準備する
デプロイ前には DUST が必要です。DUST は、トランザクションを作る・証明する・送るための燃料(手数料)です。
wallet sync 後、wallet.stop() の前に追加します。
成功ログ:
18. deployContract に進む
ここから本番です。大事な順番:
import:
DUST ready 後、wallet.stop() より前に追加します。
成功ログ:
第一弾で「今は読まなくていい」と言った output/contract/index.js と output/keys output/zkir が、ここで全部主役になりました。zkConfigProvider がそれらを読み、proofProvider が proof-server に渡して証明を作ります。
19. CostModel エラーと依存バージョン問題
実は、最初の deploy ではこのエラーが出ました。
見立て:JavaScript では、同じ名前の CostModel でも、別々の場所・別バージョンから読み込まれていると、instanceof 的に別物になることがあります。「同じはずのクラスが別物として扱われている」状態です。
調べるコマンド:
ledger-v8 は 8.0.3 に揃っていました。でも、別のズレが見つかりました。
つまり compact-runtime が 0.15.0 と 0.16.0 の二重状態。一方、生成された index.js は checkRuntimeVersion("0.16.0") を要求しています。そこで pnpm.overrides で 0.16.0 に寄せます。
確認:
修正後:
これで再実行すると、deploy に成功しました。
バージョンの噛み合わせは Support matrix(外部リンク・別タブで開きます) が正本です。「最新どうし」が必ず合うとは限らない、を思い出してください。
20. デプロイアドレスを保存する
deploy に成功しただけでは、あとで呼び出すときにアドレスを忘れてしまいます。network.ts の recordDeployment(...) で保存します。
deploy 成功直後:
21. もう不要になった確認コードを消す
デプロイが通ったら、序盤の「確認用コード」はもう役目を終えています。
これは消してOKです。理由:
ただし注意。generated を使うコードを消すなら、generated の import もセットで消すこと。import だけコメントアウトして使う側を残すと、こうなります。
最後に残すのは、これだけ。
22. 最終的な成功まとめ
ここまでの到達点を、チェックで振り返ります。
一歩ずつ確認してきたので、もしどこかでエラーが出ても、すぐ前のステップに戻れます。それが実験ノート方式の強みです。
✍️ 白紙練習(何度でも反復)
答えを開く前に、いったん自分の手から出してみましょう。デプロイは登場人物が多いので、順番を言えることがいちばんの力になります。
ローカル Midnight に Counter をデプロイするまでの大きな流れを、順番に書いてみよう(契約 → … → Contract Address)。
契約を書く → compact compile → 生成された contract/index.js を読む → CLI を作る → Midnight.js SDK を入れる → network / wallet を準備 → wallet sync → tNight 残高を見る → DUST を準備 → providers を作る → deployContract → Contract Address が出る。
process.cwd() は何を指す? なぜ deploy.ts の場所と混同してはいけない?
process.cwd() は コマンドを実行した場所(カレントディレクトリ)。deploy.ts が置いてある場所ではない。counter-01 直下から実行するなら counter-01 を指す。だから生成物のパスは path.resolve(process.cwd(), "contract/output/contract/index.js") のように、実行場所を基準に組む。
依存を cli/ ではなく counter-01 直下に入れたのはなぜ?
Node.js は import しているファイルの近くから上へさかのぼって node_modules を探す。import するのは contract/output/contract/index.js なので、counter-01/node_modules は通り道だが、cli/node_modules は兄弟ディレクトリで通り道に入らず見つからないことがある。だから依存は counter-01 直下に一本化する。
Midnight TS プロジェクトで package.json に必ず入れる1行は? 入れないと何が起きる?
"type": "module"(pnpm pkg set type=module)。入れないと Node.js が CommonJS 扱いし、ESM 前提の wallet 系で ERR_PACKAGE_PATH_NOT_EXPORTED(No “exports” main defined)などのエラーになる。
NetworkId と setNetworkId / getNetworkId のちがいは?
NetworkId は TypeScript 上の型(実行時の値ではない)。setNetworkId(...) / getNetworkId() が実行時に使う関数。ローカルでは setNetworkId("undeployed") を使う。
deployContract の前に必要な準備を、思い出せるだけ挙げてみよう。
Docker 3点セット(node / indexer / proof-server)が起動 → SDK 依存を入れる → setNetworkId → wallet 作成 → wallet sync → tNight 残高確認 → DUST 準備(ready) → CompiledContract.make → NodeZkConfigProvider / indexerPublicDataProvider / httpClientProofProvider / levelPrivateStateProvider / walletProvider で providers を組む。そのうえで deployContract(providers, { compiledContract, args: [] })。
CostModel エラー(expected instance of CostModel)の正体は? どう直した?
Counter の書き方ミスではなく、依存バージョンの混在が原因の可能性が高い。compact-runtime が 0.15.0 と 0.16.0 で二重になっていた。生成コードは checkRuntimeVersion("0.16.0") を要求するので、pnpm.overrides で compact-runtime を 0.16.0 に寄せて pnpm install → 通った。
今日の呪文
今日の勝利条件
今日のまとめ
- 今回は 契約 → コンパイル → デプロイ まで。
incrementの呼び出しは次回(第三弾)。 process.cwd()は「実行した場所」。パスは実行場所を基準に組む。- 依存は
counter-01直下に一本化。Node は import するファイルの近くから上へnode_modulesを探すから。 - Midnight TS は ESM 前提。
"type": "module"を最初から入れる。 - デプロイ前の準備= wallet sync → tNight → DUST ready → providers。
CostModelエラーは契約ミスではなく依存の混在。pnpm.overridesでバージョンを揃えて解決。- エラーは先生。「壊れた」ではなく「次に足りない部品を教えてくれている」。
次回予告(第三弾)
ここまでで、Counter 契約はローカル Midnight に置かれました。ただし、まだ increment は呼んでいません。第三弾では、保存した Contract Address を使って callTx で increment を呼び、ledger の round が変わるところまで進みます。
➡️ 第三弾はこちら:Counter 白紙練習 第三弾:callTx で increment を呼び出す
つぎに読むページ
- いよいよ呼び出し(第三弾)→ Counter 白紙練習 第三弾:callTx で increment を呼び出す
- 契約そのものを作り変えて慣れる → Counter 改造10本ノック
- 部品の全体像をもう一度 → DApp の組み立てかた(5つの部品)
- proof server と取引の流れを、もっと正確に → 2.1 Proof Server と取引の流れ
- CLI とデプロイの本物のコード(bboard 版) → 3.3 けいじ板DApp — CLIとデプロイ
- 第一弾に戻る → Counter 白紙練習