TypeScriptでEVMの判定分岐を再現する
HTTP 200を見て処理を続行する前に、レスポンス内のdecisionを確認します。TxGuardでは、simulation成功とpolicy上の許可は別の状態です。本稿では4つの合成ケースを実行し、SDK利用時の分岐まで確認します。
EVMトランザクションを組み立てるエージェントでは、送信前に「callが成功するか」と「アプリの方針として進めてよいか」を分けて扱う必要があります。
TxGuardの判定層を使って、同じsimulation成功でもALLOW・WARN・BLOCKが変わる例をローカルで再現します。対象は、すでにEVM callを作っているTypeScript開発者です。ウォレット接続や送信は行いません。
この記事では、TxGuardの判定層とSDKの動作をローカルで検証しています。公開APIの稼働、SDKのnpm公開、顧客利用や収益は今回確認していません。以下の出力は合成fixtureであり、実際のチェーンでの成功を示しません。
外部RPCなしで再現する
確認環境はNode.js v24.19.0です。以下は記事だけで実行できるJavaScriptの最小モデルです。editorial-example.mjs として保存してください。非公開リポジトリやnpmパッケージへのアクセスは不要です。
このモデルは無制限approvalの3設定と結果不明の4ケースに絞った説明用コードです。TxGuard本体が持つチェーン・アドレス・value等の検査は省いているので、実運用の判定器には使わないでください。4ケースの出力が、本体の evaluateDecision に合成fixtureを渡した結果と一致することを確認しました。
import assert from 'node:assert/strict';
// 記事用の最小モデル。TxGuard全体の実装ではない。
// 外部通信なし。calldataはcanonicalなapproveを模した合成データ。
const data = `0x095ea7b3${'0'.repeat(24)}${'33'.repeat(20)}${'f'.repeat(64)}`;
function decideApproval(data, mode, simulationStatus) {
if (!['allow', 'warn', 'block'].includes(mode)) throw new Error('Invalid mode');
if (simulationStatus !== 'SUCCESS') {
return { simulation: 'UNKNOWN', decision: 'BLOCK', warnings: [] };
}
if (!/^0x095ea7b30{24}[0-9a-f]{104}$/i.test(data)) {
throw new Error('This example accepts canonical approve calldata only');
}
const amount = BigInt(`0x${data.slice(74)}`);
const unlimited = amount === (1n << 256n) - 1n;
return {
simulation: 'SUCCESS',
decision: unlimited ? { allow: 'ALLOW', warn: 'WARN', block: 'BLOCK' }[mode] : 'ALLOW',
warnings: unlimited ? ['UNLIMITED_APPROVAL'] : [],
};
}
for (const [mode, expected] of [['allow', 'ALLOW'], ['warn', 'WARN'], ['block', 'BLOCK']]) {
const result = decideApproval(data, mode, 'SUCCESS');
assert.equal(result.decision, expected);
assert.equal(result.warnings[0], 'UNLIMITED_APPROVAL');
console.log(`${mode}: ${result.simulation} / ${result.decision} / ${result.warnings[0]}`);
}
const missing = decideApproval(data, 'block', null);
assert.equal(missing.decision, 'BLOCK');
console.log(`missing: ${missing.simulation} / ${missing.decision}`);
保存したディレクトリで実行します。
node editorial-example.mjs
確認した出力です。
allow: SUCCESS / ALLOW / UNLIMITED_APPROVAL
warn: SUCCESS / WARN / UNLIMITED_APPROVAL
block: SUCCESS / BLOCK / UNLIMITED_APPROVAL
missing: UNKNOWN / BLOCK
calldataとsimulationのSUCCESSは説明用fixtureです。EVMやRPCには問い合わせていません。本体との比較ではブロック42・gas 21000を含む合成成功結果を使いましたが、上の最小モデルはそれらの値を利用しません。最後のケースでは成功結果そのものがないため、UNKNOWN / BLOCKになります。実際のHTTPサービスではpolicyはサーバー管理であり、クライアントが任意のpolicyをリクエストに混ぜて上書きするAPIではありません。
無制限approvalを題材にする
ERC-20の approve(spender, amount) は、spenderに指定量までの利用を認めるインターフェースです。[ERC-20仕様]
ここでは approve(address,uint256) のcanonicalなcalldataに最大uint256を入れます。TxGuardはこれを UNLIMITED_APPROVAL として検出し、設定に応じて扱いを変えます。
tokenApproval.unlimited |
simulation fixture | decision | warnings |
|---|---|---|---|
allow |
SUCCESS | ALLOW | UNLIMITED_APPROVAL |
warn |
SUCCESS | WARN | UNLIMITED_APPROVAL |
block |
SUCCESS | BLOCK | UNLIMITED_APPROVAL |
ALLOWでもwarningsが空とは限りません。 明示的にallowしたfindingは、情報として残ります。反対にSUCCESSでもpolicy違反ならBLOCKです。
これはcalldataの検査です。宛先が本当にERC-20を実装している証明ではなく、permitやmulticall、他の権限付与形式は対象外です。maxTransactionValue もnative通貨のweiを制限する値であり、ERC-20数量やドル建て損失の上限ではありません。
3つの成功を分ける
| 観測するもの | 分かること | それだけでは分からないこと |
|---|---|---|
| HTTP 200 |
/v1/check が完了した判定を返した |
判定がALLOWかどうか |
simulation.status: SUCCESS |
判定層が有効な成功結果として受け取った | プロジェクトのpolicyに適合するか |
decision: ALLOW |
設定されたチェックが停止条件を出さなかった | 安全性、将来の成功、経済的な妥当性 |
Ethereumの eth_call は、指定したブロックの状態を使ってcallを実行し、チェーンにトランザクションを作らずに戻り値を返します。送信後の実行を予約する仕組みではありません。[Ethereum JSON-RPC仕様]
TxGuardの判定層では、成功結果に対して許可チェーン、送信元・送信先・approval spenderの禁止アドレス、native value上限、対応するapprovalの設定を適用します。停止理由があればBLOCK、それがなく対応が必要な警告があればWARN、それもなければALLOWです。RPC失敗や結果不明を「警告だけ」にして先へ進める設計ではありません。
guard()の戻り値も確認する
以下は未公開SDKの実装上の契約を説明するものです。npmから取得できる公開チュートリアルではありません。SDKの check() はBLOCKを含む判定を返し、guard() はBLOCKで TxGuardBlockedError を投げます。ただし、guard() はWARNでは例外を投げません。
警告時に自動処理を保留したいアプリなら、次のような分岐を別途設けます。これは接続済みの txguard と検査対象の tx を受け取る組み込み方の例であり、上のオフライン再現コードとは別です。
// SDKをローカルtarball等で導入済みのアプリ内で使用する型
import type { TxGuard, Transaction } from '@txguard/sdk';
async function mayContinue(txguard: TxGuard, tx: Transaction) {
try {
const result = await txguard.guard(tx);
if (result.decision !== 'ALLOW') {
return { next: 'REVIEW', result }; // WARNはアプリ側でレビュー
}
return { next: 'APP_CHECKS', result }; // 追加の業務条件へ
} catch {
return { next: 'STOP' }; // BLOCK・通信障害などで送信へ進まない
}
}
ここでは送信関数を呼びません。ALLOWの後も、アプリ固有の承認条件を確認します。検査後に to、value、data などを変えたトランザクションへ結果を流用してはいけません。状態変化により、検査時と送信時の結果が違う場合もあります。
通信失敗、タイムアウト、認証失敗、不正なレスポンスはSDKの例外経路です。HTTP 200だけを見て送信したり、catch後に送信処理へ落ちたりしない構造にします。
このコードと画面デモ、APIの違い
| 対象 | 契約・役割 | 今回の確認範囲 |
|---|---|---|
| Webのdemoモード | 選んだscenarioでfixtureを返す | 実RPC simulationではないことをソース確認 |
既存 POST /preflight
|
schemaVersion / chain / calldata を受けるx402系統 |
今回は決済も実行も検証しない |
SaaS POST /v1/check
|
chainId / from / to / value / data とBearer認証、サーバーpolicyによる判定 |
ソースで契約を確認。公開サービスの稼働確認ではない |
@txguard/sdk |
/v1/check の型付きHTTPクライアント |
ローカルテスト。npm公開を前提にしない |
/preflight の入力をそのまま /v1/check に渡すことはできません。またSDK自体はsimulationエンジンではなく、署名や送信も行いません。画面で成功例を選べることを、実トランザクションが成功した証拠には使えません。
検証した範囲
今回のローカル実行では、判定層12件、SDK60件が成功し、失敗・skipは0件でした。両パッケージのtypecheckも成功しています。記事の4ケースは別途実行し、assertと出力を確認しました。
以下は開発者側で実行した検証コマンドの記録です。リポジトリは非公開のため、読者の再現手順は上の単体JavaScriptを利用してください。
npm test --prefix packages/firewall
npm run typecheck --prefix packages/firewall
npm ci --cache /tmp/txguard-editorial-sdk-cache --prefix packages/sdk
npm test --prefix packages/sdk
npm run typecheck --prefix packages/sdk
これは判定分岐とクライアント契約の検証です。HTTP API全体、外部RPC、本番デプロイ、課金、コントラクトの安全性を検証した結果ではありません。
判定層とSDKの実装・テスト、HTTP APIの仕様、画面デモの実装を照合して確認しています。
自分のエージェントで評価するなら
まず、既知の成功・revert・結果不明に加え、「simulationは成功しても止めたいcall」を一つ選びます。秘密鍵や実資金を使わず、公開可能な合成fixtureで期待decisionを書いてから、実際の出力と比較してください。
見るのは判定の正答率だけではありません。「このreason codeで次の処理を決められるか」「WARN時の担当者や再確認条件を決められるか」「今のRPCエラー処理に比べて何が簡単になったか」を記録すると、導入価値を判断できます。
共有する場合は、秘密や個人情報を含まないfixture、期待したdecision、実際のdecision、不足した項目に絞ってください。公開APIへの有料利用をお願いする段階ではありません。運営者が用意した評価環境の提供範囲を確認したうえで、具体的な失敗ケースを評価するための材料です。