Crypto OS
Technical Crypto OS第3段階 · フルスタックのオンチェーンアプリケーション

T14 · Web3 フロントエンド

取引の確認を待つ十数秒間、画面には何を表示すべきか?

この章を 3D で体験する →
練習する能力
Builder
実践
取引の完全な状態機械を実装する:署名待ち、送信済み、承認待ち、成功、失敗、置き換え済み。
AI Lab
AI に取引失敗のエラーコードを列挙させ、平易な表示文を作らせて、ユーザーを誤解させる表現がないか確認する。

現実の問題

ユーザーが「交換」を押します。

ウォレットが開き、確認すると閉じます。ページには回転するアイコンと「処理中」が表示されます。

そして十数秒が過ぎます。アプリに慣れた人には長い時間です。止まったかと疑い、別アプリへ移って戻り、もう一度ボタンを押すほど長いのです。

その間、画面は実際には何も分かっていません。

  • 署名したのか?ウォレットで拒否したのに、ページは回り続けるかもしれません。
  • ブロードキャストした取引はメモリプールへ入ったか?手数料が低く、止まっていないか?
  • ブロックへ入ったか?入れば成功なのか?
  • ウォレットで「高速化」を押した後、手元のハッシュはまだ有効か?

一つでも答えを誤れば、事実と違う状態を表示します。最も高くつく誤りは、失敗した取引を成功と表示することです。

UI の細部ではありません。Web2 の要求は数百ミリ秒で成否が分かります。チェーンの取引には 6 種類の状態と、十数秒から数分の中間状態があり、中間状態は後戻りもします。Web2 と同じ感覚で画面を作ると誤ります。

思考実験

取引を HTTP 要求ではなく、書留郵便として考えます。

投函した時点から、次のことが起きます。

  • 手元に受付番号がある。これがあって初めて照会でき、なければ何もできません。
  • 郵便が仕分け場で待つ。郵送料が高ければ前へ進み、低ければ何日も待つかもしれません。
  • 郵便がある車へ載る。しかし輸送中で、事故や返送の可能性があります。
  • 車が到着する。ようやく結果が分かり、配達完了と配達失敗による返送の 2 つがあります。

最後が鍵です。到着することと、配達に成功することは別です。

チェーンでは、取引がブロックへ入るのは、マイナー/検証者が実行したことを示すだけで、成功したとは限りません。実行中にエラーとなってもブロックへ入り、手数料は消費され、レシートには失敗と記録されます。

T1 でも説明しましたが、この分野で最もよくあるバグなので繰り返します。

成否は、ブロックにあるかではなく、レシートの status を確認します。

書留にはない性質もあります。同じ番号で、別の郵便へ置き換えられます。 同じ nonce の取引が最終的にチェーンへ入るのは一つです。ウォレットの高速化やキャンセルは、同じ nonce で新しい取引を送ります。元のハッシュでは、その後レシートを取得できなくなります。

それを待ち続ける画面は、永遠に回転します。

あなたが決める

十数秒の待機画面を、どう設計しますか?

結果を観察する

違いは、最初の 3 つが画面を描き、4 つ目がモデルを作ることです。

画面中心だと、例外ごとに if が増え、5 つ目には誰も状態の総数を説明できなくなります。モデルなら、例外を状態機械のノードとして追加できます。

取引の状態は、6 つに整理できます。

状態意味画面の動作終状態か
署名待ちウォレットへ要求し、操作を待つウォレットでの確認を促し、ボタンを無効化いいえ
ブロードキャスト済みハッシュを取得し、メモリプールへ入るハッシュとエクスプローラーリンクを表示し、高速化を許可いいえ
確認待ちブロックへ入り、確認数が不足確認進捗を表示いいえ
成功レシート status が成功で、確認数も十分結果表示とデータ更新はい
失敗レシート status が失敗原因と次の行動を説明はい
置換済み同 nonce の別取引がチェーンへ入る新ハッシュを追跡するか、キャンセルを通知はい

見落としがちなルールを 2 つ加えます。

署名拒否は失敗ではありません。 エラーを出さず待機へ戻します。赤いエラーは、単に考えを変えたユーザーに、悪いことをしたと感じさせます。

成功から後戻りすることもあります。 チェーン先端の再編成が起きます(T1 の最後の実例)。確認数が不足したまま成功と表示すると、再編成後に虚偽になります。十分な確認数に達するまで、成功は本当の終状態ではありません。

モデルを作る

主経路は一直線です。

  1. Idle:待機
  2. AwaitingSignature:署名待ち
  3. Broadcast:送信済み
  4. Pending:確認待ち
  5. Confirmed:成功
これは順調な経路です。実際の作業量は、各ノードから分かれる経路にあります。

型で状態を記述します。言語より重要なのは、各状態の持つデータが異なることです。ブール値ではなく、ユニオン型を使う理由です。

type TxState =
  | { kind: 'idle' }
  | { kind: 'awaitingSignature'; intent: TxIntent }
  | { kind: 'broadcast';  hash: string; nonce: number; submittedAt: number }
  | { kind: 'pending';    hash: string; nonce: number; blockNumber: number; confirmations: number }
  | { kind: 'confirmed';  hash: string; blockNumber: number; confirmations: number }
  | { kind: 'failed';     hash?: string; reason: FailureReason }
  | { kind: 'replaced';   originalHash: string; replacementHash: string; by: 'speedup' | 'cancel' | 'unknown' };

type FailureReason =
  | 'userRejected'        // ウォレットで拒否。厳密には失敗ではない
  | 'insufficientFunds'   // 手数料または送金額の残高不足
  | 'reverted'            // チェーンへ入ったが実行が巻き戻された
  | 'slippage'            // 見積期限切れやスリッページ超過。reverted の一種
  | 'nonceConflict'       // nonce を別の取引が使った
  | 'dropped'             // 長時間ブロックへ入らず、メモリプールから除外された
  | 'unknown';

isLoading、isSuccess、isError の 3 ブール値では、不可能な isLoading && isSuccess も型として有効で、いつか分岐で現れます。ユニオン型は不正な状態を表現不可能にします。最も実際的な利点です。

遷移表

各遷移は、個別にテストできるケースです。

遷移元きっかけ遷移先
待機ユーザーのクリック署名待ち
署名待ちハッシュ取得送信済み
署名待ちユーザーの拒否待機(失敗ではない)
署名待ちウォレットのエラー失敗
送信済みブロック内で取引を発見確認待ち
送信済み時間切れでハッシュも不明失敗(dropped)
送信済み同 nonce の別取引が入る置換済み
確認待ちレシート成功と必要確認数成功
確認待ちレシート失敗失敗(reverted)
確認待ち親ハッシュが不連続で再編成送信済みへ戻る
成功確認数が減り、再編成確認待ちへ戻る

最後の 2 行が表の価値です。状態機械は後戻りを許す必要があります。 前進だけでは再編成時に誤った終状態に固まり、成功表示が永久に修正されません。

間違えやすい 3 点

第一に、nonce を中心に、ハッシュを補助として追跡します。 高速化やキャンセルでハッシュは変わり、nonce は変わりません。そのアドレスの確定済み nonce が対象 nonce を超えたのに、対象ハッシュのレシートがなければ、置換済みです。実際に入った同 nonce のハッシュを探し、追跡を続けます。

第二に、待機は中断、再開できるようにします。 更新、タブ終了、端末変更が起きます。追跡対象をローカルへ永続化し、読み込み時に未完了の取引の追跡を再開します。メモリだけなら、一度の更新で失われます。

第三に、送信済みではなく、署名待ちでボタンを無効にします。 両者の間の数秒に再度押すと、二つ目の署名要求が出ます。この隙間では重複送信が必然的に起きます。まれな出来事ではありません。

エラーの変換表

低水準のエラーを、説明と行動へ変えます。3 列目が最も重要です。

元の状況ユーザーへの説明できる行動
署名拒否表示せず静かに戻る再び押す
手数料残高不足手数料が不足しています。基軸コインを補充してください入金入口へ移動
実行の巻き戻し取引は完了せず、手数料は消費されました原因候補を見て再試行
スリッページ超過設定した範囲以上に価格が変わりました許容幅の変更か再見積もり
長時間未収録ネットワークが混雑し、取引は待機中です高速化かキャンセル
置換済みこの取引は新しい取引に置き換わりました新しい取引を確認

文言の必須ルールは 2 つです。

  • レシート取得前に成功と伝えません。 提出済み、送信済み、確認中は使えても、成功は使えません。
  • 理由なしに「失敗。再試行してください」と書きません。 再び失敗し、もう一度手数料を失う可能性が高いからです。

何と呼ぶか

Transaction State Machine取引の状態機械

有限の状態と明確な遷移条件で、取引の寿命全体を表します。

コンポーネントに散らばるブール値群を置き換えます。Crypto のフロントエンドの質を見るなら、これがあるかを確認すればよいのです。

Optimistic UI楽観的 UI 更新

取引確定前に期待される結果を画面へ反映し、即時に感じさせます。

必須の対となる機能はロールバックです。失敗、置換、再編成では元へ戻し、通知します。戻せない更新は、良い体験ではなく善意の嘘です。

Error Mappingエラーマッピング

ノード、ウォレット、コントラクトの生エラーを、理解できる説明と実行可能な次の行動へ変えます。

各 catch へ散らさず、中央の一つの表にします。そうして初めて、レビュー、テスト、再利用ができます。

Reorg Handling再編成への対応

チェーン先端が巻き戻ると、成功表示した取引を未確認へ戻し、必要なら楽観的更新も取り消します。

フロントエンドではバックエンドより単純で、状態機械が後戻りを許せばよいのです。本格的な複雑さは T17 で扱います。

Replacement取引の置換

同じ nonce と高い手数料で新取引を送り、元の取引を置き換えます。高速化もキャンセルもこれです。

フロントエンドへの意味は、元ハッシュではレシートを取得できず、nonce で実態を発見する必要があることです。

Confirmations確認数

取引のブロックの後に生成されたブロック数です。真偽値ではなく、確率的な安全指標です。

必要数は業務の判断です。アバター変更なら 1 でもよく、大口引き出しの許可なら数十が必要な場合があります。

実践

実践一つの取引へ、完全な 6 状態の状態機械を実装する任意のフロントエンドフレームワーク + テストネット + ブロックエクスプローラー0 円、すべてテストネットで実施

フォーセットのテストコインだけを使い、テストネットで行います。 わざと失敗や停滞を作るので、メインネットで練習しないでください。

目標は美しい画面ではなく、6 状態をすべて実際に一度通ることです。

コンポーネントより先に型を書きます。

前のユニオン型をコピーし、現在の状態とイベントから新状態を返す、純粋な reducer を書きます。

合格基準は、reducer にネットワーク呼び出しがないことです。純粋なら、通信なしで全遷移をテストできます。

順調な経路を接続し、成功へ進みます。

単純なテストネット送金を行います。ハッシュ取得で送信済みへ進み、レシートをポーリングし、status が成功かつ確認数が十分なら成功へ進みます。

status を必ず検証します。 status が成功でないのに成功状態へ入ったら例外を投げる、一行のアサーションを書きます。本章の中心です。

失敗へ進みます。

必ず巻き戻される取引を作ります。例えば残高を超える送金額など、条件を満たさない引数をコントラクトへ渡します。

取引はチェーンへ入り、手数料を消費し、レシート status は失敗になります。画面が成功ではなく失敗を表示するか確認します。

成功と出たなら、業界で最も多いバグを、自分のコードで再現したことになります。

置換済みへ進みます。

手数料を現在よりはるかに低くして送信し、メモリプールに停滞させます。

ハッシュと nonce を記録し、ウォレットで高速化かキャンセルを押します。同じ nonce の新取引が送られます。

画面は、確定済み nonce が対象を超えたのに元ハッシュのレシートがないと検出し、置換済みへ進み、新ハッシュへ追跡を切り替えるべきです。

回り続けるなら、nonce を追わずハッシュだけを追っています。

署名待ちから待機へ戻ります。

取引を開始し、ウォレットで拒否します。

静かに待機へ戻り、ボタンが使えるようになり、赤いエラーは出さないのが正解です。出るなら直してください。拒否は誤りではありません。

後戻りを作ります。

再編成の発生は難しいので、reducer へ確認数減少イベントを直接送り、成功から確認待ちへ戻り、表示が「再確認中」になるか試します。

数行の純粋なロジックテストですが、設計の最も重要な性質、後戻りできる状態機械を検証します。

エラー表を追加し、更新も試します。

前の表を一つの集中マッピングとして実装します。取引を送り、未確定の間にページを更新します。

消えるのではなく、追跡が再開されるべきです。できなければ、状態がメモリだけにあります。

7 工程を終えれば、どのプロジェクトへも持ち込める状態機械ができます。この段階で最も再利用しやすいコードです。

AI Lab

AI LabAI に取引エラーと分かりやすい文言を作らせ、誤解を誘わないか一つずつ確認するLevel 2 · AI Copilot

2 段階で進め、必要な成果は 2 番目です。

第 1 段階:
チェーンの取引を作成してから確定するまでの、全失敗状況を列挙してください。
各状況の条件、ユーザーが見る現象、具体的な判定フィールドを示してください。
署名拒否、手数料不足、巻き戻し、スリッページ超過、nonce 衝突、
メモリプールからの除外、同 nonce の新取引による置換を含めてください。

第 2 段階:
各状況について、一般ユーザー向けの日本語メッセージを作ってください。
- 20 文字以内
- 何が起きたか分かる
- 今できる行動を示す
その後、文言が、取引成功、着金済み、引き落とし済み、取り消し可能という
誤解を生まないか、一つずつ自己点検してください。

モデルは自己点検が得意でも、結果は自分で再確認しなければなりません。文言はどれも自然に読めるのが危険です。「完了」と「提出済み」は語感では近くても、意味には status 一つ分の差があります。

ほぼ必ずある偏りは、置換済みを漏らしやすいことです。多くの文書やチュートリアルに登場せず、実ユーザーが高速化したときに初めて現れます。漏れたら、「高速化後、元のハッシュはどうなるか」と直接聞いてください。

AI の回答後、自分で必ず検証すること

  • チェーンへ入ったことを成功と同一視していない。最重要項目として、すべての文言を確認した
  • レシート前の段階で、成功、完了、着金という言葉を使っていない
  • 署名拒否をエラーとして赤字にしていない
  • 置換済みの状態を扱っている。多くのモデルが漏らします
  • エラーコード、フィールド名、判定条件を、実際のウォレットとノードの戻り値でそれぞれ確認した
  • 各文言に、単なる後での再試行ではなく、今実行できる行動が付いている
  • 手数料不足と資産残高不足を区別した。解決法がまったく異なります

実例

送信済みを成功と表示した

最もよくあるバグで、多くの新プロジェクトが一度は犯します。

緑の「交換成功」を見たユーザーがウォレットを開くと、トークンは交換されず、手数料だけ減っています。レシート status は失敗ですが、フロントエンドは読まず、ハッシュ取得時点で成功にしていました。

見つけにくいのは、正常経路では完全に正常に見えることです。失敗時にだけ現れ、開発時には失敗取引を専用に作る人が少ないためです。

Lab の第 3 工程で、意図的に作る理由です。

高速化後、画面が永遠に回転する

ユーザーが高速化すると、取引はすぐチェーンへ入りましたが、ページは回り続けます。

高速化は同じ nonce の新ハッシュを使います。フロントエンドは永久にレシートのない旧ハッシュをポーリングしていました。

さらに悪い場合は、何回か後に失敗と表示します。実際には成功したのに、画面は失敗を伝えます。ユーザーは再送するかもしれません。

楽観的更新にロールバックがない

クリック直後に一覧へ交換成功を追加し、失敗後も残ります。

次回訪問時に一覧が更新され、記録が消えます。ユーザーは、元から失敗したとは思わず、製品が記録をなくしたと考えます。

戻せない楽観的更新が生むのは、錯覚ではなく不信です。

モバイルで背景へ移り、署名のコールバックを失った

ブラウザーからウォレットへ移り、確認後に戻る間に、OS がページを破棄して再読み込みする場合があります。

実際には取引を送ったのに表示がなく、ユーザーは失敗と思って再送します。両方の取引が実行されます。

永続化の直接的な理由です。読み込み時に、進行中の取引をローカルから復元できなければなりません。

変数を一つ変える

もしブロック生成が 1 秒未満のチェーンへ変える

状態機械は一文字も変えず、中間状態の滞在時間だけが短くなります。

ただし生成が速いほど一ブロックの重みが軽く、先端の再編成確率が通常高くなることは重要です。同じ 12 確認でも、チェーンにより安全性が大きく異なります。

確認数を全体の定数にせず、チェーンと業務ごとの設定にします。

もしユーザーが一括実行可能なコントラクトウォレットを使う

一クリックが、複数操作を含む取引になり、2 つの変化を生みます。

良い変化:承認と操作を一工程にでき、クリックが減り、承認だけ成功して操作が失敗する中間状態がありません。

難しい変化:レシート status は一つでも、内部には複数操作があり、ログで失敗箇所を判定します。一取引一エラーから、一取引一組の結果へマッピングを拡張します。T27 で詳しく扱います。

もし手数料を別の人がユーザーに代わって払う

基軸コインが不要になり、手数料不足が流れから消え、体験が大きく改善します。

しかし前段階が増えます。代払い側が支払う意思を持つか確認します。拒否、制限、実行途中の失敗があり、新しい失敗要因を表へ入れます。

また取引の発信者がユーザーではなくなる場合があり、nonce の追跡対象も変えます。

もし製品がオフライン署名を必須にする

ネット未接続の端末で署名し、署名済み取引を接続端末へ運んで送信します。

署名待ちと送信済みの間に、数分から数日の手動移送の空白ができ、状態機械が二段に分かれます。

署名時には正しかった nonce が、移送後には別取引に使われ、取引期限切れがよくある失敗になります。時間が延びると、無視できた競合が日常になります。

持ち帰る問い

4
ユーザーは誰か?

ユーザーは誰ですか?チェーンの仕組みを知らず、自分のお金がどうなったかを知りたい人です。16 進数は不要でも、失敗を含む真の状態を知る権利があります。

5
誰が支払っているか?

誰が支払いますか?失敗してもユーザーが手数料を払います。失敗文言に明記しないと、ユーザーは資金を盗んだと疑ってから、別の場所で聞くことになります。

9
誰がリスクを負うか?

誰がリスクを負いますか?失敗を成功と表示すると、損失はユーザーへ、しかも気付かれずに落ちます。誤認したまま次の操作へ進みます。状態機械は表示ミスを、運任せから網羅的にテストできる問題へ変えます。

章末チェック

要点

暗号資産のフロントエンドの中心は取引の状態機械であり、ページの寄せ集めではない。

この章の実践課題を終えたらチェックしてください進捗をすべて見る

このページの内容