diff --git a/Cargo.lock b/Cargo.lock index 5967688..1a2ab1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1924,7 +1924,7 @@ checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "beam-cli" -version = "0.2.3" +version = "0.2.4" dependencies = [ "argon2", "async-trait", diff --git a/beam-apps/README.md b/beam-apps/README.md index 364ce0e..736e3c4 100644 --- a/beam-apps/README.md +++ b/beam-apps/README.md @@ -43,6 +43,7 @@ In another shell: export BEAM_APP_REGISTRY_URL=http://127.0.0.1:8787 export BEAM_HOME="$(mktemp -d)" cargo run -p beam-cli --bin beam -- apps install uniswap --dry-run +cargo run -p beam-cli --bin beam -- apps install erc8004 --dry-run ``` Set `BEAM_UNISWAP_PUBLIC_API_KEY` before starting `run-local.py` when testing @@ -65,11 +66,16 @@ the `payy.network` zone. App source stays separate from Beam core. Product apps live under `beam-apps/apps/` and must not path-depend on `pkg/*` crates or inherit root -workspace dependencies. The Uniswap app is its own Rust workspace; CI installs -`wasm32-unknown-unknown`, injects the Payy-managed public Uniswap API key from -the `BEAM_UNISWAP_PUBLIC_API_KEY` GitHub secret, builds its release WASM, -verifies the generated registry bundle, and bakes only the signed static bundle -into the registry image. +workspace dependencies. The Uniswap and ERC-8004 apps are independent Rust +workspaces; CI installs `wasm32-unknown-unknown`, injects the Payy-managed public +Uniswap API key from the `BEAM_UNISWAP_PUBLIC_API_KEY` GitHub secret, builds +release WASM for every app, verifies the generated registry bundle, and bakes +only the signed static bundle into the registry image. + +ERC-8004 agent identity management ships as the `erc8004` registry app. Beam CLI +provides only generic app-host capabilities for it: bounded `eth_getLogs`, chain +calls, invocation-scoped contract overrides, typed-data signing, and approved +action-plan execution. Until a shared app SDK crate is published, product apps may vendor app-local host ABI structs. Beam CLI remains the generic host/runtime and must not contain diff --git a/beam-apps/apps/erc8004/Cargo.lock b/beam-apps/apps/erc8004/Cargo.lock new file mode 100644 index 0000000..5366915 --- /dev/null +++ b/beam-apps/apps/erc8004/Cargo.lock @@ -0,0 +1,319 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "beam-app-erc8004" +version = "1.0.0" +dependencies = [ + "ethabi", + "hex", + "serde", + "serde_json", + "sha2", + "sha3", + "thiserror", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ethabi" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" +dependencies = [ + "ethereum-types", + "hex", + "sha3", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "fixed-hash", + "primitive-types", + "uint", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/beam-apps/apps/erc8004/Cargo.toml b/beam-apps/apps/erc8004/Cargo.toml new file mode 100644 index 0000000..c9c6946 --- /dev/null +++ b/beam-apps/apps/erc8004/Cargo.toml @@ -0,0 +1,19 @@ +[workspace] + +[package] +name = "beam-app-erc8004" +version = "1.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +ethabi = { version = "18", default-features = false } +hex = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" +sha3 = "0.10" +thiserror = "2" diff --git a/beam-apps/apps/erc8004/README.md b/beam-apps/apps/erc8004/README.md new file mode 100644 index 0000000..43734c1 --- /dev/null +++ b/beam-apps/apps/erc8004/README.md @@ -0,0 +1,30 @@ +# ERC-8004 Beam App + +The ERC-8004 app manages identity-registry agents through Beam's generic app +host. It keeps registry defaults and overrides in app space rather than as a +native Beam command. + +```text +beam x erc8004 support +beam x erc8004 config show +beam x erc8004 config set --identity-registry
+beam x erc8004 register [--uri |--empty-uri] [--identity-registry
] +beam x erc8004 show [--fetch-uri] [--identity-registry
] +beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--identity-registry
] +beam x erc8004 set-uri [--identity-registry
] +beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
] +beam x erc8004 unset-wallet [--identity-registry
] +``` + +Default ERC-8004 identity registry addresses are manifest-scoped. Custom +registry addresses come from app-local config or an explicit +`--identity-registry` flag and are included as invocation-scoped contract rules +in host calls and action plans. + +`list` uses `eth_getLogs` through the Beam host. The host enforces a bounded +block range and the app defaults to the active wallet with owner filtering, so +it does not scan from genesis unless the user passes a broad explicit range. + +`set-wallet` resolves the wallet argument through Beam and requests an EIP-712 +typed-data signature from the host. The app receives only the signature and +digest, never raw private keys. diff --git a/beam-apps/apps/erc8004/assets/icon.svg b/beam-apps/apps/erc8004/assets/icon.svg new file mode 100644 index 0000000..91a16e6 --- /dev/null +++ b/beam-apps/apps/erc8004/assets/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/beam-apps/apps/erc8004/manifest.json b/beam-apps/apps/erc8004/manifest.json new file mode 100644 index 0000000..3041e57 --- /dev/null +++ b/beam-apps/apps/erc8004/manifest.json @@ -0,0 +1,426 @@ +{ + "format_version": 1, + "id": "erc8004", + "display_name": "ERC-8004", + "version": "1.0.0", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "min_beam_version": "0.2.4", + "wasm": { + "sha256": "", + "entrypoint": "beam_app_main" + }, + "icon": { + "path": "assets/icon.svg", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + }, + "catalog": { + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": ["Includes the identity registry and whether it is default or overridden."] + } + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": ["Registry overrides are stored in app-local Beam storage."] + } + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": ["Returns an action plan that Beam approves and executes."] + } + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": ["Non-HTTPS URIs are not fetched."] + } + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": ["The host caps log ranges and response size."] + } + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": ["Returns an action plan."] + } + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": ["The app never receives raw private keys."] + } + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "sensitive_args": [], + "input_schema": { "type": "object" }, + "output_schema": { "type": "object" }, + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": ["Returns an action plan."] + } + } + ], + "permissions": { + "http": [ + { + "url": "https://*" + } + ], + "chains": [ + { + "chain": "*", + "operations": ["read", "logs", "send-transaction", "sign-typed-data"], + "contracts": [ + "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "0x8004A818BFB912233c491871b3d84c89A494BD9e" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ] + } + ], + "wallet": { + "read_balances": false, + "propose_transactions": true, + "erc20_approval": false, + "sign_typed_data": true + }, + "storage": { + "app_local": true + }, + "privacy": [] + }, + "host_api": { + "privacy_reserved": true + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "" + } +} diff --git a/beam-apps/apps/erc8004/src/abi.rs b/beam-apps/apps/erc8004/src/abi.rs new file mode 100644 index 0000000..689780e --- /dev/null +++ b/beam-apps/apps/erc8004/src/abi.rs @@ -0,0 +1,231 @@ +use ethabi::{ + ParamType, Token, decode, encode, + ethereum_types::{Address, U256}, +}; +use sha3::{Digest, Keccak256}; + +use crate::{Error, Result, host::LogEntry}; + +pub const AGENT_WALLET_DOMAIN_NAME: &str = "ERC8004IdentityRegistry"; +pub const AGENT_WALLET_DOMAIN_VERSION: &str = "1"; +pub const REGISTERED_EVENT_SIGNATURE: &str = "Registered(uint256,string,address)"; + +pub fn selector(signature: &str) -> String { + let hash = keccak(signature.as_bytes()); + format!("0x{}", hex::encode(&hash[..4])) +} + +pub fn register_calldata(uri: Option<&str>) -> String { + match uri { + Some(uri) => calldata("register(string)", &[Token::String(uri.to_string())]), + None => calldata("register()", &[]), + } +} + +pub fn set_uri_calldata(agent_id: U256, uri: &str) -> String { + calldata( + "setAgentURI(uint256,string)", + &[Token::Uint(agent_id), Token::String(uri.to_string())], + ) +} + +pub fn unset_wallet_calldata(agent_id: U256) -> String { + calldata("unsetAgentWallet(uint256)", &[Token::Uint(agent_id)]) +} + +pub fn set_wallet_calldata( + agent_id: U256, + wallet: Address, + deadline: u64, + signature: &str, +) -> Result { + let signature = hex_bytes(signature)?; + Ok(calldata( + "setAgentWallet(uint256,address,uint256,bytes)", + &[ + Token::Uint(agent_id), + Token::Address(wallet), + Token::Uint(U256::from(deadline)), + Token::Bytes(signature), + ], + )) +} + +pub fn owner_of_calldata(agent_id: U256) -> String { + calldata("ownerOf(uint256)", &[Token::Uint(agent_id)]) +} + +pub fn token_uri_calldata(agent_id: U256) -> String { + calldata("tokenURI(uint256)", &[Token::Uint(agent_id)]) +} + +pub fn get_agent_wallet_calldata(agent_id: U256) -> String { + calldata("getAgentWallet(uint256)", &[Token::Uint(agent_id)]) +} + +pub fn decode_address(raw: &str) -> Result
{ + let tokens = decode(&[ParamType::Address], &hex_bytes(raw)?).map_err(|err| { + Error::InvalidHostResponse { + reason: format!("{err:?}"), + } + })?; + match tokens.as_slice() { + [Token::Address(value)] => Ok(*value), + _ => Err(Error::InvalidHostResponse { + reason: "address response had wrong ABI shape".to_string(), + }), + } +} + +pub fn decode_string(raw: &str) -> Result { + let tokens = decode(&[ParamType::String], &hex_bytes(raw)?).map_err(|err| { + Error::InvalidHostResponse { + reason: format!("{err:?}"), + } + })?; + match tokens.as_slice() { + [Token::String(value)] => Ok(value.clone()), + _ => Err(Error::InvalidHostResponse { + reason: "string response had wrong ABI shape".to_string(), + }), + } +} + +pub fn registered_topic() -> String { + format!( + "0x{}", + hex::encode(keccak(REGISTERED_EVENT_SIGNATURE.as_bytes())) + ) +} + +pub fn parse_registered_event(log: &LogEntry, registry: &str) -> Option { + if !log.address.eq_ignore_ascii_case(registry) || log.topics.len() != 3 { + return None; + } + if log.topics.first()?.to_ascii_lowercase() != registered_topic() { + return None; + } + + let agent_id = parse_u256_word(log.topics.get(1)?)?; + let owner = address_from_topic(log.topics.get(2)?)?; + let uri = decode(&[ParamType::String], &hex_bytes(&log.data).ok()?) + .ok() + .and_then(|tokens| match tokens.as_slice() { + [Token::String(value)] => Some(value.clone()), + _ => None, + })?; + + Some(RegisteredEvent { + agent_id, + owner, + uri, + }) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegisteredEvent { + pub agent_id: U256, + pub owner: Address, + pub uri: String, +} + +pub fn agent_wallet_hashes( + chain_id: u64, + verifying_contract: Address, + agent_id: U256, + new_wallet: Address, + owner: Address, + deadline: u64, +) -> (String, String) { + let domain_separator = keccak(&encode(&[ + bytes32_token(&keccak( + b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)", + )), + bytes32_token(&keccak(AGENT_WALLET_DOMAIN_NAME.as_bytes())), + bytes32_token(&keccak(AGENT_WALLET_DOMAIN_VERSION.as_bytes())), + Token::Uint(U256::from(chain_id)), + Token::Address(verifying_contract), + ])); + let struct_hash = keccak(&encode(&[ + bytes32_token(&keccak( + b"AgentWalletSet(uint256 agentId,address newWallet,address owner,uint256 deadline)", + )), + Token::Uint(agent_id), + Token::Address(new_wallet), + Token::Address(owner), + Token::Uint(U256::from(deadline)), + ])); + + ( + format!("0x{}", hex::encode(domain_separator)), + format!("0x{}", hex::encode(struct_hash)), + ) +} + +pub fn parse_address(value: &str) -> Result
{ + value.parse::
().map_err(|_| Error::InvalidAddress { + value: value.to_string(), + }) +} + +pub fn parse_agent_id(value: &str) -> Result { + if let Some(value) = value.strip_prefix("0x") { + return U256::from_str_radix(value, 16).map_err(|_| Error::InvalidAgentId { + value: format!("0x{value}"), + }); + } + value.parse::().map_err(|_| Error::InvalidAgentId { + value: value.to_string(), + }) +} + +pub fn address_hex(address: Address) -> String { + format!("{address:#x}") +} + +pub fn calldata_hash(data: &str) -> String { + format!( + "sha256:{}", + hex::encode(sha2::Sha256::digest(data.as_bytes())) + ) +} + +fn calldata(signature: &str, tokens: &[Token]) -> String { + let selector = selector(signature); + format!("{selector}{}", hex::encode(encode(tokens))) +} + +fn bytes32_token(bytes: &[u8; 32]) -> Token { + Token::FixedBytes(bytes.to_vec()) +} + +fn keccak(bytes: &[u8]) -> [u8; 32] { + let mut hasher = Keccak256::new(); + hasher.update(bytes); + hasher.finalize().into() +} + +fn hex_bytes(value: &str) -> Result> { + hex::decode(value.strip_prefix("0x").unwrap_or(value)).map_err(|_| Error::InvalidHostResponse { + reason: format!("invalid hex value {value}"), + }) +} + +fn parse_u256_word(value: &str) -> Option { + let bytes = hex::decode(value.strip_prefix("0x").unwrap_or(value)).ok()?; + if bytes.len() != 32 { + return None; + } + Some(U256::from_big_endian(&bytes)) +} + +fn address_from_topic(value: &str) -> Option
{ + let bytes = hex::decode(value.strip_prefix("0x").unwrap_or(value)).ok()?; + if bytes.len() != 32 { + return None; + } + Some(Address::from_slice(&bytes[12..])) +} + +pub type AgentId = U256; +pub type EvmAddress = Address; diff --git a/beam-apps/apps/erc8004/src/args.rs b/beam-apps/apps/erc8004/src/args.rs new file mode 100644 index 0000000..d1077c6 --- /dev/null +++ b/beam-apps/apps/erc8004/src/args.rs @@ -0,0 +1,331 @@ +use crate::{Error, Result}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Command { + Support, + ConfigShow, + ConfigSet(ConfigSetArgs), + Register(RegisterArgs), + Show(ShowArgs), + List(ListArgs), + SetUri(SetUriArgs), + SetWallet(SetWalletArgs), + UnsetWallet(UnsetWalletArgs), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ConfigSetArgs { + pub identity_registry: String, + pub reputation_registry: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegisterArgs { + pub uri: Option, + pub identity_registry: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ShowArgs { + pub agent_id: String, + pub fetch_uri: bool, + pub identity_registry: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ListArgs { + pub connection: ConnectionMode, + pub from_block: Option, + pub identity_registry: Option, + pub to_block: Option, + pub wallet: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SetUriArgs { + pub agent_id: String, + pub identity_registry: Option, + pub uri: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SetWalletArgs { + pub agent_id: String, + pub deadline_seconds: u64, + pub identity_registry: Option, + pub wallet: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UnsetWalletArgs { + pub agent_id: String, + pub identity_registry: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ConnectionMode { + Owner, + AgentWallet, + Both, +} + +impl ConnectionMode { + pub fn label(&self) -> &'static str { + match self { + Self::Owner => "owner", + Self::AgentWallet => "agent-wallet", + Self::Both => "both", + } + } +} + +pub fn parse(args: &[String]) -> Result { + let command = args + .first() + .map(String::as_str) + .ok_or_else(|| Error::UnsupportedCommand { + command: "".to_string(), + })?; + match command { + "support" => Ok(Command::Support), + "config" => parse_config(args), + "register" => parse_register(args), + "show" => parse_show(args), + "list" => parse_list(args), + "set-uri" => parse_set_uri(args), + "set-wallet" => parse_set_wallet(args), + "unset-wallet" => parse_unset_wallet(args), + other => Err(Error::UnsupportedCommand { + command: other.to_string(), + }), + } +} + +fn parse_config(args: &[String]) -> Result { + match args.get(1).map(String::as_str) { + Some("show") => Ok(Command::ConfigShow), + Some("set") => { + let mut identity_registry = None; + let mut reputation_registry = None; + let mut index = 2; + while index < args.len() { + match args[index].as_str() { + "--identity-registry" => { + identity_registry = + Some(parse_next(args, &mut index, "--identity-registry")?) + } + "--reputation-registry" => { + reputation_registry = + Some(parse_next(args, &mut index, "--reputation-registry")?) + } + other => return unsupported_flag(other), + } + index += 1; + } + Ok(Command::ConfigSet(ConfigSetArgs { + identity_registry: identity_registry.ok_or_else(|| Error::InvalidArgument { + reason: "config set requires --identity-registry".to_string(), + })?, + reputation_registry, + })) + } + other => Err(Error::UnsupportedCommand { + command: format!("config {}", other.unwrap_or("")), + }), + } +} + +fn parse_register(args: &[String]) -> Result { + let mut uri = None; + let mut empty_uri = false; + let mut identity_registry = None; + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--uri" => uri = Some(parse_next(args, &mut index, "--uri")?), + "--empty-uri" => empty_uri = true, + "--identity-registry" => { + identity_registry = Some(parse_next(args, &mut index, "--identity-registry")?) + } + other => return unsupported_flag(other), + } + index += 1; + } + if uri.is_some() && empty_uri { + return Err(Error::InvalidArgument { + reason: "register accepts either --uri or --empty-uri".to_string(), + }); + } + Ok(Command::Register(RegisterArgs { + uri: if empty_uri { None } else { uri }, + identity_registry, + })) +} + +fn parse_show(args: &[String]) -> Result { + let agent_id = args.get(1).cloned().ok_or_else(|| Error::InvalidArgument { + reason: "show requires ".to_string(), + })?; + let mut fetch_uri = false; + let mut identity_registry = None; + let mut index = 2; + while index < args.len() { + match args[index].as_str() { + "--fetch-uri" => fetch_uri = true, + "--identity-registry" => { + identity_registry = Some(parse_next(args, &mut index, "--identity-registry")?) + } + other => return unsupported_flag(other), + } + index += 1; + } + Ok(Command::Show(ShowArgs { + agent_id, + fetch_uri, + identity_registry, + })) +} + +fn parse_list(args: &[String]) -> Result { + let mut connection = ConnectionMode::Owner; + let mut from_block = None; + let mut identity_registry = None; + let mut to_block = None; + let mut wallet = None; + let mut index = 1; + while index < args.len() { + match args[index].as_str() { + "--connection" => { + connection = match parse_next(args, &mut index, "--connection")?.as_str() { + "owner" => ConnectionMode::Owner, + "agent-wallet" => ConnectionMode::AgentWallet, + "both" => ConnectionMode::Both, + value => { + return Err(Error::InvalidArgument { + reason: format!("invalid connection mode {value}"), + }); + } + } + } + "--from-block" => from_block = Some(parse_u64_flag(args, &mut index, "--from-block")?), + "--identity-registry" => { + identity_registry = Some(parse_next(args, &mut index, "--identity-registry")?) + } + "--to-block" => to_block = Some(parse_u64_flag(args, &mut index, "--to-block")?), + "--wallet" => wallet = Some(parse_next(args, &mut index, "--wallet")?), + other => return unsupported_flag(other), + } + index += 1; + } + Ok(Command::List(ListArgs { + connection, + from_block, + identity_registry, + to_block, + wallet, + })) +} + +fn parse_set_uri(args: &[String]) -> Result { + let agent_id = args.get(1).cloned().ok_or_else(|| Error::InvalidArgument { + reason: "set-uri requires ".to_string(), + })?; + let uri = args.get(2).cloned().ok_or_else(|| Error::InvalidArgument { + reason: "set-uri requires ".to_string(), + })?; + let mut identity_registry = None; + let mut index = 3; + while index < args.len() { + match args[index].as_str() { + "--identity-registry" => { + identity_registry = Some(parse_next(args, &mut index, "--identity-registry")?) + } + other => return unsupported_flag(other), + } + index += 1; + } + Ok(Command::SetUri(SetUriArgs { + agent_id, + identity_registry, + uri, + })) +} + +fn parse_set_wallet(args: &[String]) -> Result { + let agent_id = args.get(1).cloned().ok_or_else(|| Error::InvalidArgument { + reason: "set-wallet requires ".to_string(), + })?; + let wallet = args.get(2).cloned().ok_or_else(|| Error::InvalidArgument { + reason: "set-wallet requires ".to_string(), + })?; + let mut deadline_seconds = 300; + let mut identity_registry = None; + let mut index = 3; + while index < args.len() { + match args[index].as_str() { + "--deadline-seconds" => { + deadline_seconds = parse_u64_flag(args, &mut index, "--deadline-seconds")?; + } + "--identity-registry" => { + identity_registry = Some(parse_next(args, &mut index, "--identity-registry")?) + } + other => return unsupported_flag(other), + } + index += 1; + } + if deadline_seconds > 300 { + return Err(Error::InvalidArgument { + reason: "set-wallet deadline cannot exceed 300 seconds".to_string(), + }); + } + Ok(Command::SetWallet(SetWalletArgs { + agent_id, + deadline_seconds, + identity_registry, + wallet, + })) +} + +fn parse_unset_wallet(args: &[String]) -> Result { + let agent_id = args.get(1).cloned().ok_or_else(|| Error::InvalidArgument { + reason: "unset-wallet requires ".to_string(), + })?; + let mut identity_registry = None; + let mut index = 2; + while index < args.len() { + match args[index].as_str() { + "--identity-registry" => { + identity_registry = Some(parse_next(args, &mut index, "--identity-registry")?) + } + other => return unsupported_flag(other), + } + index += 1; + } + Ok(Command::UnsetWallet(UnsetWalletArgs { + agent_id, + identity_registry, + })) +} + +fn parse_next(args: &[String], index: &mut usize, flag: &str) -> Result { + *index += 1; + args.get(*index) + .cloned() + .ok_or_else(|| Error::InvalidArgument { + reason: format!("{flag} requires a value"), + }) +} + +fn parse_u64_flag(args: &[String], index: &mut usize, flag: &str) -> Result { + parse_next(args, index, flag)? + .parse::() + .map_err(|_| Error::InvalidArgument { + reason: format!("{flag} must be an integer"), + }) +} + +fn unsupported_flag(flag: &str) -> Result { + Err(Error::InvalidArgument { + reason: format!("unsupported erc8004 flag {flag}"), + }) +} diff --git a/beam-apps/apps/erc8004/src/config.rs b/beam-apps/apps/erc8004/src/config.rs new file mode 100644 index 0000000..790cb20 --- /dev/null +++ b/beam-apps/apps/erc8004/src/config.rs @@ -0,0 +1,275 @@ +use serde_json::{Value, json}; + +use crate::{ + Error, Result, + abi::parse_address, + host::{self, DynamicContractScope}, +}; + +const LEGACY_STORAGE_KEY: &str = "registry-config-v1"; +const MAINNET_IDENTITY_REGISTRY: &str = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"; +const MAINNET_REPUTATION_REGISTRY: &str = "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63"; +const TESTNET_IDENTITY_REGISTRY: &str = "0x8004A818BFB912233c491871b3d84c89A494BD9e"; +const TESTNET_REPUTATION_REGISTRY: &str = "0x8004B663056A597Dffe9eCcC1965A193B7388713"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegistryConfig { + pub chain_id: u64, + pub display_name: String, + pub identity_registry: String, + pub is_default_identity: bool, + pub reputation_registry: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RegistrySelection { + pub config: RegistryConfig, + pub dynamic_contracts: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +struct StoredConfig { + #[serde(default)] + chains: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +struct StoredChainConfig { + chain_id: u64, + identity_registry: String, + reputation_registry: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct Deployment { + chain_id: u64, + display_name: &'static str, + identity_registry: &'static str, + reputation_registry: &'static str, +} + +pub fn select( + chain_id: u64, + chain_display_name: &str, + override_identity: Option<&str>, +) -> Result { + let stored_chain = load_stored_chain_config(chain_id)?; + let default = deployment_for_chain_id(chain_id); + let identity_registry = override_identity + .map(normalize_address) + .transpose()? + .or_else(|| { + stored_chain + .as_ref() + .map(|chain| chain.identity_registry.clone()) + }) + .or_else(|| { + default + .as_ref() + .map(|deployment| deployment.identity_registry.to_string()) + }) + .ok_or(Error::UnsupportedChain { chain_id })?; + let reputation_registry = stored_chain + .as_ref() + .map(|chain| chain.reputation_registry.clone()) + .or_else(|| { + default + .as_ref() + .map(|deployment| deployment.reputation_registry.to_string()) + }) + .unwrap_or_else(|| TESTNET_REPUTATION_REGISTRY.to_string()); + let default_identity = default + .as_ref() + .map(|deployment| deployment.identity_registry) + .unwrap_or_default(); + let is_default_identity = identity_registry.eq_ignore_ascii_case(default_identity); + let dynamic_contracts = (!is_default_identity) + .then(|| DynamicContractScope { + chain: chain_display_name.to_string(), + contract: identity_registry.clone(), + reason: "ERC-8004 identity registry override".to_string(), + }) + .into_iter() + .collect(); + + Ok(RegistrySelection { + config: RegistryConfig { + chain_id, + display_name: default + .as_ref() + .map(|deployment| deployment.display_name.to_string()) + .unwrap_or_else(|| chain_display_name.to_string()), + identity_registry, + is_default_identity, + reputation_registry, + }, + dynamic_contracts, + }) +} + +pub fn show(chain_id: u64, chain_display_name: &str) -> Result { + Ok(select(chain_id, chain_display_name, None)?.config) +} + +pub fn set( + chain_id: u64, + chain_display_name: &str, + identity_registry: &str, + reputation_registry: Option<&str>, +) -> Result { + let identity_registry = normalize_address(identity_registry)?; + let default = deployment_for_chain_id(chain_id); + let reputation_registry = reputation_registry + .map(normalize_address) + .transpose()? + .unwrap_or_else(|| { + default + .clone() + .map(|deployment| deployment.reputation_registry.to_string()) + .unwrap_or_else(|| TESTNET_REPUTATION_REGISTRY.to_string()) + }); + let default_identity = default + .as_ref() + .map(|deployment| deployment.identity_registry) + .unwrap_or_default(); + let is_default_identity = identity_registry.eq_ignore_ascii_case(default_identity); + let display_name = default + .as_ref() + .map(|deployment| deployment.display_name.to_string()) + .unwrap_or_else(|| chain_display_name.to_string()); + let stored = StoredChainConfig { + chain_id, + identity_registry: identity_registry.clone(), + reputation_registry: reputation_registry.clone(), + }; + let stored = serde_json::to_string(&stored).map_err(|err| Error::Serialization { + reason: err.to_string(), + })?; + host::storage_set(&chain_storage_key(chain_id), &stored)?; + Ok(RegistryConfig { + chain_id, + display_name, + identity_registry, + is_default_identity, + reputation_registry, + }) +} + +pub fn to_json(config: &RegistryConfig) -> Value { + json!({ + "chain_id": config.chain_id, + "display_name": config.display_name, + "identity_registry": config.identity_registry, + "identity_registry_source": if config.is_default_identity { "default" } else { "override" }, + "reputation_registry": config.reputation_registry, + }) +} + +fn load_stored_chain_config(chain_id: u64) -> Result> { + if let Some(value) = host::storage_get(&chain_storage_key(chain_id))? { + return Ok(Some(parse_storage_value(value)?)); + } + + let Some(value) = host::storage_get(LEGACY_STORAGE_KEY)? else { + return Ok(None); + }; + Ok(parse_storage_value::(value)? + .chains + .into_iter() + .find(|chain| chain.chain_id == chain_id)) +} + +fn parse_storage_value(value: Value) -> Result { + match value { + Value::String(value) => { + serde_json::from_str::(&value).map_err(|err| Error::Serialization { + reason: err.to_string(), + }) + } + value => serde_json::from_value::(value).map_err(|err| Error::Serialization { + reason: err.to_string(), + }), + } +} + +fn chain_storage_key(chain_id: u64) -> String { + format!("registry-config-v1-{chain_id}") +} + +fn normalize_address(value: &str) -> Result { + Ok(format!("{:#x}", parse_address(value)?)) +} + +fn deployment_for_chain_id(chain_id: u64) -> Option { + DEPLOYMENTS + .iter() + .find(|deployment| deployment.chain_id == chain_id) + .cloned() +} + +const fn mainnet(chain_id: u64, display_name: &'static str) -> Deployment { + Deployment { + chain_id, + display_name, + identity_registry: MAINNET_IDENTITY_REGISTRY, + reputation_registry: MAINNET_REPUTATION_REGISTRY, + } +} + +const fn testnet(chain_id: u64, display_name: &'static str) -> Deployment { + Deployment { + chain_id, + display_name, + identity_registry: TESTNET_IDENTITY_REGISTRY, + reputation_registry: TESTNET_REPUTATION_REGISTRY, + } +} + +const DEPLOYMENTS: &[Deployment] = &[ + mainnet(1, "Ethereum Mainnet"), + testnet(11155111, "Ethereum Sepolia"), + mainnet(8453, "Base Mainnet"), + testnet(84532, "Base Sepolia"), + mainnet(2741, "Abstract Mainnet"), + testnet(11124, "Abstract Testnet"), + mainnet(42161, "Arbitrum Mainnet"), + testnet(421614, "Arbitrum Testnet"), + mainnet(43114, "Avalanche Mainnet"), + testnet(43113, "Avalanche Testnet"), + mainnet(56, "BSC Mainnet"), + testnet(97, "BSC Testnet"), + mainnet(42220, "Celo Mainnet"), + testnet(11142220, "Celo Testnet"), + mainnet(100, "Gnosis Mainnet"), + mainnet(2345, "GOAT Network Mainnet"), + mainnet(59144, "Linea Mainnet"), + testnet(59141, "Linea Sepolia"), + mainnet(5000, "Mantle Mainnet"), + testnet(5003, "Mantle Testnet"), + mainnet(4326, "MegaETH Mainnet"), + testnet(6343, "MegaETH Testnet"), + mainnet(1088, "Metis Mainnet"), + testnet(59902, "Metis Sepolia"), + mainnet(143, "Monad Mainnet"), + testnet(10143, "Monad Testnet"), + mainnet(10, "Optimism Mainnet"), + testnet(11155420, "Optimism Testnet"), + mainnet(137, "Polygon Mainnet"), + testnet(80002, "Polygon Amoy"), + mainnet(534352, "Scroll Mainnet"), + testnet(534351, "Scroll Testnet"), + mainnet(1187947933, "SKALE Base Mainnet"), + testnet(324705682, "SKALE Base Sepolia"), + mainnet(1868, "Soneium Mainnet"), + testnet(1946, "Soneium Minato"), + mainnet(167000, "Taiko Mainnet"), + testnet(167012, "Taiko Hoodi"), + mainnet(196, "XLayer Mainnet"), + testnet(1952, "XLayer Testnet"), + testnet(296, "Hedera Testnet"), + testnet(5042002, "Arc Testnet"), + mainnet(45056, "Billions Mainnet"), + testnet(6913, "Billions Testnet"), + mainnet(1776, "Injective Mainnet"), + testnet(1439, "Injective Testnet"), +]; diff --git a/beam-apps/apps/erc8004/src/error.rs b/beam-apps/apps/erc8004/src/error.rs new file mode 100644 index 0000000..95bf578 --- /dev/null +++ b/beam-apps/apps/erc8004/src/error.rs @@ -0,0 +1,34 @@ +pub type Result = std::result::Result; + +#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] +pub enum Error { + #[error("[beam-app-erc8004] unsupported command: {command}")] + UnsupportedCommand { command: String }, + + #[error("[beam-app-erc8004] invalid argument: {reason}")] + InvalidArgument { reason: String }, + + #[error("[beam-app-erc8004] unsupported ERC-8004 chain id: {chain_id}")] + UnsupportedChain { chain_id: u64 }, + + #[error("[beam-app-erc8004] invalid agent uri: {uri}")] + InvalidAgentUri { uri: String }, + + #[error("[beam-app-erc8004] invalid agent id: {value}")] + InvalidAgentId { value: String }, + + #[error("[beam-app-erc8004] address value is invalid: {value}")] + InvalidAddress { value: String }, + + #[error("[beam-app-erc8004] integer value is invalid: {value}")] + InvalidInteger { value: String }, + + #[error("[beam-app-erc8004] host call failed: {message}")] + HostCallFailed { message: String }, + + #[error("[beam-app-erc8004] host response is invalid: {reason}")] + InvalidHostResponse { reason: String }, + + #[error("[beam-app-erc8004] serialization failed: {reason}")] + Serialization { reason: String }, +} diff --git a/beam-apps/apps/erc8004/src/host.rs b/beam-apps/apps/erc8004/src/host.rs new file mode 100644 index 0000000..ddd9efb --- /dev/null +++ b/beam-apps/apps/erc8004/src/host.rs @@ -0,0 +1,434 @@ +// lint-long-file-override allow-max-lines=500 +use serde_json::Value; + +use crate::{Error, Result}; + +const HOST_API_VERSION: u32 = 1; +const HOST_RESPONSE_CAPACITY: usize = 2 * 1024 * 1024; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlanContext { + pub app_id: String, + pub app_version: String, + pub chain: String, + pub manifest_sha256: String, + pub wallet: String, + pub wasm_sha256: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct GuestInvocation { + pub args: Vec, + pub host_api_version: u32, + pub metadata: HostMetadata, + pub output_mode: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct HostMetadata { + pub app_id: String, + pub app_version: String, + pub chain: String, + pub chain_id: u64, + #[serde(default)] + pub debug: bool, + pub host_api_version: u32, + pub manifest_sha256: String, + pub now: u64, + pub wallet: String, + pub wasm_sha256: String, +} + +impl HostMetadata { + pub fn plan_context(&self) -> PlanContext { + PlanContext { + app_id: self.app_id.clone(), + app_version: self.app_version.clone(), + chain: self.chain.clone(), + manifest_sha256: self.manifest_sha256.clone(), + wallet: self.wallet.clone(), + wasm_sha256: self.wasm_sha256.clone(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ActionPlan { + pub app_id: String, + pub app_version: String, + pub wasm_sha256: String, + pub manifest_sha256: String, + pub command: String, + pub wallet: Option, + pub chain: String, + #[serde(default)] + pub steps: Vec, + #[serde(default)] + pub bindings: Vec, + #[serde(default)] + pub constraints: Vec, + #[serde(default)] + pub dynamic_contracts: Vec, + pub expires_at: u64, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct DynamicContractScope { + pub chain: String, + pub contract: String, + #[serde(default)] + pub reason: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ActionStep { + pub kind: String, + pub summary: String, + pub target: Option, + pub selector: Option, + pub spender: Option, + pub value: Option, + #[serde(default)] + pub metadata: Value, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ActionBinding { + pub key: String, + pub value: String, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +enum HostRequest { + HttpFetch(HttpFetchRequest), + ChainRead(ChainReadRequest), + SignTypedData(TypedDataSignRequest), + Diagnostic { level: String, message: String }, + ResolveAddress { value: Option }, + AppStorageGet { key: String }, + AppStorageSet { key: String, value: String }, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct HttpFetchRequest { + method: String, + url: String, + headers: Vec, + body: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct HttpHeader { + name: String, + value: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct HttpFetchResponse { + pub body: Vec, + pub status: u16, + pub url: String, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct ChainReadRequest { + chain: String, + operation: ChainReadOperation, + address: Option, + data: Option, + dynamic_contracts: Vec, + from_block: Option, + owner: Option, + spender: Option, + target: Option, + token: Option, + topics: Vec>>, + to_block: Option, + value: Option, + selector: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +enum ChainReadOperation { + Call, + Logs, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct TypedDataSignRequest { + chain: String, + dynamic_contracts: Vec, + domain_separator: String, + fields: Vec, + primary_type: String, + struct_hash: String, + verifying_contract: String, + wallet: String, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct TypedDataDisplayField { + name: String, + kind: String, + value: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct HostCallResponse { + ok: bool, + value: Option, + error: Option, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct StorageGetResponse { + exists: bool, + value: Option, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct CallResponse { + pub raw: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct LogsResponse { + pub logs: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct LogEntry { + pub address: String, + pub data: String, + pub topics: Vec, +} + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct SignatureResponse { + pub digest: String, + pub signature: String, + pub signer: String, +} + +pub fn ensure_host_abi(invocation: &GuestInvocation) -> Result<()> { + if invocation.host_api_version < HOST_API_VERSION + || invocation.metadata.host_api_version < HOST_API_VERSION + { + return Err(Error::InvalidHostResponse { + reason: format!( + "unsupported host abi version {}", + invocation.host_api_version + ), + }); + } + + Ok(()) +} + +pub fn eth_call( + chain: &str, + target: &str, + data: &str, + dynamic_contracts: &[DynamicContractScope], +) -> Result { + let response = chain_read(ChainReadRequest { + address: None, + chain: chain.to_string(), + data: Some(data.to_string()), + dynamic_contracts: dynamic_contracts.to_vec(), + from_block: None, + operation: ChainReadOperation::Call, + owner: None, + selector: selector_from_calldata(data), + spender: None, + target: Some(target.to_string()), + token: None, + topics: Vec::new(), + to_block: None, + value: None, + })?; + Ok(parse_host_value::(response)?.raw) +} + +pub fn logs( + chain: &str, + target: &str, + topics: Vec>>, + from_block: Option, + to_block: Option, + dynamic_contracts: &[DynamicContractScope], +) -> Result { + let response = chain_read(ChainReadRequest { + address: None, + chain: chain.to_string(), + data: None, + dynamic_contracts: dynamic_contracts.to_vec(), + from_block, + operation: ChainReadOperation::Logs, + owner: None, + selector: None, + spender: None, + target: Some(target.to_string()), + token: None, + topics, + to_block, + value: None, + })?; + parse_host_value(response) +} + +pub fn sign_typed_data( + chain: &str, + wallet: &str, + verifying_contract: &str, + domain_separator: &str, + struct_hash: &str, + fields: Vec<(&str, &str, String)>, + dynamic_contracts: &[DynamicContractScope], +) -> Result { + let response = host_call(HostRequest::SignTypedData(TypedDataSignRequest { + chain: chain.to_string(), + dynamic_contracts: dynamic_contracts.to_vec(), + domain_separator: domain_separator.to_string(), + fields: fields + .into_iter() + .map(|(kind, name, value)| TypedDataDisplayField { + kind: kind.to_string(), + name: name.to_string(), + value, + }) + .collect(), + primary_type: "AgentWalletSet".to_string(), + struct_hash: struct_hash.to_string(), + verifying_contract: verifying_contract.to_string(), + wallet: wallet.to_string(), + }))?; + parse_host_value(response) +} + +pub fn http_get(url: &str) -> Result { + let response = host_call(HostRequest::HttpFetch(HttpFetchRequest { + method: "GET".to_string(), + url: url.to_string(), + headers: Vec::new(), + body: Vec::new(), + }))?; + parse_host_value(response) +} + +pub fn resolve_address(value: Option<&str>) -> Result { + let response = host_call(HostRequest::ResolveAddress { + value: value.map(str::to_string), + })?; + Ok(response + .get("address") + .and_then(Value::as_str) + .ok_or_else(|| Error::InvalidHostResponse { + reason: "resolve address response missing address".to_string(), + })? + .to_string()) +} + +pub fn storage_get(key: &str) -> Result> { + let response = host_call(HostRequest::AppStorageGet { + key: key.to_string(), + })?; + let response = parse_host_value::(response)?; + if response.exists { + Ok(response.value) + } else { + Ok(None) + } +} + +pub fn storage_set(key: &str, value: &str) -> Result<()> { + host_call(HostRequest::AppStorageSet { + key: key.to_string(), + value: value.to_string(), + })?; + Ok(()) +} + +fn chain_read(request: ChainReadRequest) -> Result { + host_call(HostRequest::ChainRead(request)) +} + +fn selector_from_calldata(data: &str) -> Option { + let data = data.strip_prefix("0x").unwrap_or(data); + (data.len() >= 8).then(|| format!("0x{}", &data[..8])) +} + +fn host_call(request: HostRequest) -> Result { + let request = serde_json::to_vec(&request).map_err(|err| Error::Serialization { + reason: err.to_string(), + })?; + let mut response = vec![0_u8; HOST_RESPONSE_CAPACITY]; + let len = beam_host_call_wrapper(&request, &mut response)?; + let response = serde_json::from_slice::(&response[..len]).map_err(|err| { + Error::InvalidHostResponse { + reason: err.to_string(), + } + })?; + if !response.ok { + return Err(Error::HostCallFailed { + message: response + .error + .unwrap_or_else(|| "host call failed without message".to_string()), + }); + } + response.value.ok_or_else(|| Error::InvalidHostResponse { + reason: "successful host response missing value".to_string(), + }) +} + +fn beam_host_call_wrapper(request: &[u8], response: &mut [u8]) -> Result { + #[cfg(target_arch = "wasm32")] + { + let len = unsafe { + beam_host_call( + request.as_ptr(), + request.len(), + response.as_mut_ptr(), + response.len(), + ) + }; + if len < 0 { + return Err(Error::HostCallFailed { + message: format!("host response exceeded buffer: {} bytes", -len), + }); + } + usize::try_from(len).map_err(|_| Error::InvalidHostResponse { + reason: format!("invalid host response length {len}"), + }) + } + + #[cfg(not(target_arch = "wasm32"))] + { + let _ = request; + let _ = response; + Err(Error::HostCallFailed { + message: "host calls are only available in wasm guest execution".to_string(), + }) + } +} + +fn parse_host_value(value: Value) -> Result +where + T: serde::de::DeserializeOwned, +{ + serde_json::from_value::(value).map_err(|err| Error::InvalidHostResponse { + reason: err.to_string(), + }) +} + +#[cfg(target_arch = "wasm32")] +unsafe extern "C" { + fn beam_host_call( + request_ptr: *const u8, + request_len: usize, + response_ptr: *mut u8, + response_capacity: usize, + ) -> i32; +} diff --git a/beam-apps/apps/erc8004/src/lib.rs b/beam-apps/apps/erc8004/src/lib.rs new file mode 100644 index 0000000..aea63c4 --- /dev/null +++ b/beam-apps/apps/erc8004/src/lib.rs @@ -0,0 +1,483 @@ +// lint-long-file-override allow-max-lines=700 +mod abi; +mod args; +mod config; +mod error; +mod host; +mod plan; + +pub use abi::selector; +pub use error::{Error, Result}; +pub use host::{ActionBinding, ActionPlan, ActionStep, GuestInvocation, PlanContext}; + +use abi::{ + AgentId, EvmAddress, address_hex, agent_wallet_hashes, decode_address, decode_string, + get_agent_wallet_calldata, owner_of_calldata, parse_address, parse_agent_id, + parse_registered_event, register_calldata, registered_topic, set_uri_calldata, + set_wallet_calldata, token_uri_calldata, unset_wallet_calldata, +}; +use args::{Command, ConnectionMode}; +use ethabi::ethereum_types::Address; +use plan::{TransactionPlanInput, agent_binding, binding, transaction_plan, wallet_binding}; +use serde_json::{Value, json}; + +#[cfg(test)] +mod tests; + +#[derive(Clone, Debug, Eq, PartialEq)] +struct Agent { + agent_id: AgentId, + agent_wallet: EvmAddress, + owner: EvmAddress, + uri: String, +} + +#[unsafe(no_mangle)] +pub extern "C" fn beam_alloc(len: usize) -> *mut u8 { + let mut buffer = Vec::::with_capacity(len); + let ptr = buffer.as_mut_ptr(); + core::mem::forget(buffer); + ptr +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn beam_free(ptr: *mut u8, capacity: usize) { + if ptr.is_null() || capacity == 0 { + return; + } + unsafe { + let _ = Vec::::from_raw_parts(ptr, 0, capacity); + } +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn beam_app_main(input_ptr: *const u8, input_len: usize) -> u64 { + let result = run_guest(input_ptr, input_len).unwrap_or_else(|error| GuestResponse::Error { + message: error.to_string(), + }); + pack_response(&result) +} + +fn run_guest(input_ptr: *const u8, input_len: usize) -> Result { + if input_ptr.is_null() { + return Err(Error::InvalidHostResponse { + reason: "guest invocation pointer is null".to_string(), + }); + } + let input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + let invocation = + serde_json::from_slice::(input).map_err(|err| Error::Serialization { + reason: err.to_string(), + })?; + host::ensure_host_abi(&invocation)?; + match args::parse(&invocation.args)? { + Command::Support => output(run_support(&invocation)?), + Command::ConfigShow => output(run_config_show(&invocation)?), + Command::ConfigSet(args) => output(run_config_set(&invocation, args)?), + Command::Register(args) => action_plan(run_register(&invocation, args)?), + Command::Show(args) => output(run_show(&invocation, args)?), + Command::List(args) => output(run_list(&invocation, args)?), + Command::SetUri(args) => action_plan(run_set_uri(&invocation, args)?), + Command::SetWallet(args) => action_plan(run_set_wallet(&invocation, args)?), + Command::UnsetWallet(args) => action_plan(run_unset_wallet(&invocation, args)?), + } +} + +fn run_support(invocation: &GuestInvocation) -> Result { + let selection = config::select( + invocation.metadata.chain_id, + &invocation.metadata.chain, + None, + )?; + let value = json!({ + "message": format!( + "ERC-8004 is supported on {} ({})\nidentity registry: {}", + selection.config.display_name, + selection.config.chain_id, + selection.config.identity_registry + ), + "supported": true, + "chain": invocation.metadata.chain, + "chain_id": invocation.metadata.chain_id, + "registry": config::to_json(&selection.config), + }); + Ok(value) +} + +fn run_config_show(invocation: &GuestInvocation) -> Result { + let config = config::show(invocation.metadata.chain_id, &invocation.metadata.chain)?; + Ok(json!({ + "message": format!("ERC-8004 identity registry: {}", config.identity_registry), + "registry": config::to_json(&config), + })) +} + +fn run_config_set(invocation: &GuestInvocation, args: args::ConfigSetArgs) -> Result { + let config = config::set( + invocation.metadata.chain_id, + &invocation.metadata.chain, + &args.identity_registry, + args.reputation_registry.as_deref(), + )?; + Ok(json!({ + "message": format!("Updated ERC-8004 registry config: {}", config.identity_registry), + "registry": config::to_json(&config), + })) +} + +fn run_register(invocation: &GuestInvocation, args: args::RegisterArgs) -> Result { + if let Some(uri) = args.uri.as_deref() { + validate_agent_uri(uri)?; + } + let selection = config::select( + invocation.metadata.chain_id, + &invocation.metadata.chain, + args.identity_registry.as_deref(), + )?; + let calldata = register_calldata(args.uri.as_deref()); + let mut bindings = vec![binding( + "agent_uri", + args.uri.as_deref().unwrap_or(""), + )]; + bindings.push(plan::selector_binding(if args.uri.is_some() { + "register(string)" + } else { + "register()" + })); + transaction_plan(TransactionPlanInput { + bindings, + calldata, + command: "register".to_string(), + context: invocation.metadata.plan_context(), + dynamic_contracts: selection.dynamic_contracts, + expires_at: invocation.metadata.now + 15 * 60, + registry: selection.config.identity_registry, + summary: "Register ERC-8004 agent".to_string(), + value: "0".to_string(), + }) +} + +fn run_show(invocation: &GuestInvocation, args: args::ShowArgs) -> Result { + let selection = config::select( + invocation.metadata.chain_id, + &invocation.metadata.chain, + args.identity_registry.as_deref(), + )?; + let agent_id = parse_agent_id(&args.agent_id)?; + let agent = read_agent( + &invocation.metadata.chain, + &selection.config.identity_registry, + &selection.dynamic_contracts, + agent_id, + )?; + let uri_body = if args.fetch_uri { + fetch_uri(agent.uri.as_str())? + } else { + None + }; + + Ok(json!({ + "message": format!("ERC-8004 agent {} owned by {}", agent.agent_id, address_hex(agent.owner)), + "agent": agent_json(&agent), + "agent_uri_body": uri_body, + "identity_registry": selection.config.identity_registry, + })) +} + +fn run_list(invocation: &GuestInvocation, args: args::ListArgs) -> Result { + let selection = config::select( + invocation.metadata.chain_id, + &invocation.metadata.chain, + args.identity_registry.as_deref(), + )?; + let wallet = host::resolve_address(args.wallet.as_deref())?; + let wallet = parse_address(&wallet)?; + let owner_topic = if matches!(args.connection, ConnectionMode::Owner) { + Some(vec![address_topic(wallet)]) + } else { + None + }; + let logs = host::logs( + &invocation.metadata.chain, + &selection.config.identity_registry, + vec![Some(vec![registered_topic()]), None, owner_topic], + args.from_block, + args.to_block, + &selection.dynamic_contracts, + )?; + let mut agents = Vec::new(); + for event in logs + .logs + .iter() + .filter_map(|log| parse_registered_event(log, &selection.config.identity_registry)) + { + let agent = read_agent( + &invocation.metadata.chain, + &selection.config.identity_registry, + &selection.dynamic_contracts, + event.agent_id, + )?; + if connects(&agent, wallet, &args.connection) { + agents.push(agent_json(&agent)); + } + } + + Ok(json!({ + "message": format!("Found {} ERC-8004 agents", agents.len()), + "agents": agents, + "connection": args.connection.label(), + "identity_registry": selection.config.identity_registry, + "wallet": address_hex(wallet), + })) +} + +fn run_set_uri(invocation: &GuestInvocation, args: args::SetUriArgs) -> Result { + validate_agent_uri(&args.uri)?; + let selection = config::select( + invocation.metadata.chain_id, + &invocation.metadata.chain, + args.identity_registry.as_deref(), + )?; + let agent_id = parse_agent_id(&args.agent_id)?; + let calldata = set_uri_calldata(agent_id, &args.uri); + transaction_plan(TransactionPlanInput { + bindings: vec![ + agent_binding(agent_id), + binding("agent_uri", &args.uri), + plan::selector_binding("setAgentURI(uint256,string)"), + ], + calldata, + command: format!("set-uri {agent_id}"), + context: invocation.metadata.plan_context(), + dynamic_contracts: selection.dynamic_contracts, + expires_at: invocation.metadata.now + 15 * 60, + registry: selection.config.identity_registry, + summary: format!("Update ERC-8004 agent {agent_id} URI"), + value: "0".to_string(), + }) +} + +fn run_unset_wallet(invocation: &GuestInvocation, args: args::UnsetWalletArgs) -> Result { + let selection = config::select( + invocation.metadata.chain_id, + &invocation.metadata.chain, + args.identity_registry.as_deref(), + )?; + let agent_id = parse_agent_id(&args.agent_id)?; + let calldata = unset_wallet_calldata(agent_id); + transaction_plan(TransactionPlanInput { + bindings: vec![ + agent_binding(agent_id), + plan::selector_binding("unsetAgentWallet(uint256)"), + ], + calldata, + command: format!("unset-wallet {agent_id}"), + context: invocation.metadata.plan_context(), + dynamic_contracts: selection.dynamic_contracts, + expires_at: invocation.metadata.now + 15 * 60, + registry: selection.config.identity_registry, + summary: format!("Clear ERC-8004 agent {agent_id} wallet"), + value: "0".to_string(), + }) +} + +fn run_set_wallet(invocation: &GuestInvocation, args: args::SetWalletArgs) -> Result { + let selection = config::select( + invocation.metadata.chain_id, + &invocation.metadata.chain, + args.identity_registry.as_deref(), + )?; + let agent_id = parse_agent_id(&args.agent_id)?; + let target_wallet = host::resolve_address(Some(&args.wallet))?; + let target_wallet = parse_address(&target_wallet)?; + let registry = parse_address(&selection.config.identity_registry)?; + let agent = read_agent( + &invocation.metadata.chain, + &selection.config.identity_registry, + &selection.dynamic_contracts, + agent_id, + )?; + let deadline = invocation + .metadata + .now + .saturating_add(args.deadline_seconds); + let (domain_separator, struct_hash) = agent_wallet_hashes( + invocation.metadata.chain_id, + registry, + agent_id, + target_wallet, + agent.owner, + deadline, + ); + let signature = host::sign_typed_data( + &invocation.metadata.chain, + &args.wallet, + &selection.config.identity_registry, + &domain_separator, + &struct_hash, + vec![ + ("uint256", "agentId", agent_id.to_string()), + ("address", "newWallet", address_hex(target_wallet)), + ("address", "owner", address_hex(agent.owner)), + ("uint256", "deadline", deadline.to_string()), + ], + &selection.dynamic_contracts, + )?; + let calldata = set_wallet_calldata(agent_id, target_wallet, deadline, &signature.signature)?; + transaction_plan(TransactionPlanInput { + bindings: vec![ + agent_binding(agent_id), + wallet_binding("agent_wallet", target_wallet), + wallet_binding("owner", agent.owner), + binding("deadline", &deadline.to_string()), + binding("signed_by", &signature.signer), + binding("typed_data_digest", &signature.digest), + binding("signature_hash", &abi::calldata_hash(&signature.signature)), + plan::selector_binding("setAgentWallet(uint256,address,uint256,bytes)"), + ], + calldata, + command: format!("set-wallet {agent_id} {}", args.wallet), + context: invocation.metadata.plan_context(), + dynamic_contracts: selection.dynamic_contracts, + expires_at: deadline, + registry: selection.config.identity_registry, + summary: format!("Update ERC-8004 agent {agent_id} wallet"), + value: "0".to_string(), + }) +} + +fn read_agent( + chain: &str, + registry: &str, + dynamic_contracts: &[host::DynamicContractScope], + agent_id: AgentId, +) -> Result { + let owner = decode_address(&host::eth_call( + chain, + registry, + &owner_of_calldata(agent_id), + dynamic_contracts, + )?)?; + let uri = decode_string(&host::eth_call( + chain, + registry, + &token_uri_calldata(agent_id), + dynamic_contracts, + )?)?; + let agent_wallet = decode_address(&host::eth_call( + chain, + registry, + &get_agent_wallet_calldata(agent_id), + dynamic_contracts, + )?)?; + + Ok(Agent { + agent_id, + agent_wallet, + owner, + uri, + }) +} + +fn fetch_uri(uri: &str) -> Result> { + if !uri.starts_with("https://") { + return Ok(None); + } + let response = host::http_get(uri)?; + let text = String::from_utf8(response.body).map_err(|err| Error::InvalidHostResponse { + reason: err.to_string(), + })?; + if !(200..300).contains(&response.status) { + return Ok(Some(json!({ + "body": sanitize_control_chars(&text), + "status": response.status, + "url": response.url, + }))); + } + match serde_json::from_str::(&text) { + Ok(value) => Ok(Some(value)), + Err(_) => Ok(Some(json!(sanitize_control_chars(&text)))), + } +} + +fn connects(agent: &Agent, wallet: Address, mode: &ConnectionMode) -> bool { + matches!(mode, ConnectionMode::Owner | ConnectionMode::Both) && agent.owner == wallet + || matches!(mode, ConnectionMode::AgentWallet | ConnectionMode::Both) + && agent.agent_wallet == wallet +} + +fn agent_json(agent: &Agent) -> Value { + json!({ + "agent_id": agent.agent_id.to_string(), + "agent_uri": agent.uri, + "agent_wallet": address_hex(agent.agent_wallet), + "owner": address_hex(agent.owner), + }) +} + +fn validate_agent_uri(uri: &str) -> Result<()> { + if uri.starts_with("https://") || uri.starts_with("ipfs://") || uri.starts_with("data:") { + Ok(()) + } else { + Err(Error::InvalidAgentUri { + uri: uri.to_string(), + }) + } +} + +fn address_topic(address: Address) -> String { + format!("0x{:0>64}", hex::encode(address.as_bytes())) +} + +fn sanitize_control_chars(value: &str) -> String { + value.chars().filter(|ch| !ch.is_control()).collect() +} + +enum GuestResponse { + Output { value: Value }, + ActionPlan { plan: ActionPlan }, + Error { message: String }, +} + +fn output(value: Value) -> Result { + Ok(GuestResponse::Output { value }) +} + +fn action_plan(plan: ActionPlan) -> Result { + Ok(GuestResponse::ActionPlan { plan }) +} + +fn pack_response(value: &GuestResponse) -> u64 { + let bytes = response_bytes(value).unwrap_or_else(|err| { + format!( + r#"{{"kind":"error","message":"[beam-app-erc8004] serialization failed: {}"}}"#, + err + ) + .into_bytes() + }); + let ptr = beam_alloc(bytes.len()); + if ptr.is_null() { + return 0; + } + unsafe { + core::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len()); + } + ((ptr as u64) << 32) | bytes.len() as u64 +} + +fn response_bytes(value: &GuestResponse) -> std::result::Result, serde_json::Error> { + match value { + GuestResponse::Output { value } => serde_json::to_vec(&json!({ + "kind": "output", + "value": value, + })), + GuestResponse::ActionPlan { plan } => serde_json::to_vec(&json!({ + "kind": "action-plan", + "plan": plan, + })), + GuestResponse::Error { message } => serde_json::to_vec(&json!({ + "kind": "error", + "message": message, + })), + } +} diff --git a/beam-apps/apps/erc8004/src/plan.rs b/beam-apps/apps/erc8004/src/plan.rs new file mode 100644 index 0000000..e5f4069 --- /dev/null +++ b/beam-apps/apps/erc8004/src/plan.rs @@ -0,0 +1,82 @@ +use serde_json::json; + +use crate::{ + Result, + abi::{AgentId, EvmAddress, address_hex, calldata_hash, selector}, + host::{ActionBinding, ActionPlan, ActionStep, DynamicContractScope, PlanContext}, +}; + +#[derive(Clone, Debug)] +pub struct TransactionPlanInput { + pub bindings: Vec, + pub calldata: String, + pub command: String, + pub context: PlanContext, + pub dynamic_contracts: Vec, + pub expires_at: u64, + pub registry: String, + pub summary: String, + pub value: String, +} + +pub fn transaction_plan(input: TransactionPlanInput) -> Result { + let selector = selector_from_calldata(&input.calldata); + let mut bindings = input.bindings; + bindings.push(binding("calldata_hash", &calldata_hash(&input.calldata))); + let registry = input.registry; + let value = input.value; + let step = ActionStep { + kind: "transaction".to_string(), + metadata: json!({ + "transaction": { + "data": input.calldata, + "to": registry.clone(), + "value": value.clone(), + }, + }), + selector, + spender: None, + summary: input.summary, + target: Some(registry), + value: Some(value), + }; + + Ok(ActionPlan { + app_id: input.context.app_id, + app_version: input.context.app_version, + wasm_sha256: input.context.wasm_sha256, + manifest_sha256: input.context.manifest_sha256, + command: input.command, + wallet: Some(input.context.wallet), + chain: input.context.chain, + steps: vec![step], + bindings, + constraints: Vec::new(), + dynamic_contracts: input.dynamic_contracts, + expires_at: input.expires_at, + }) +} + +pub fn binding(key: &str, value: &str) -> ActionBinding { + ActionBinding { + key: key.to_string(), + value: value.to_string(), + } +} + +pub fn agent_binding(agent_id: AgentId) -> ActionBinding { + binding("agent_id", &agent_id.to_string()) +} + +pub fn wallet_binding(key: &str, wallet: EvmAddress) -> ActionBinding { + binding(key, &address_hex(wallet)) +} + +fn selector_from_calldata(data: &str) -> Option { + let data = data.strip_prefix("0x").unwrap_or(data); + (data.len() >= 8).then(|| format!("0x{}", &data[..8])) +} + +pub fn selector_binding(signature: &str) -> ActionBinding { + binding("selector", &selector(signature)) +} diff --git a/beam-apps/apps/erc8004/src/tests.rs b/beam-apps/apps/erc8004/src/tests.rs new file mode 100644 index 0000000..faecdcf --- /dev/null +++ b/beam-apps/apps/erc8004/src/tests.rs @@ -0,0 +1,153 @@ +use crate::{ + abi::{ + address_hex, agent_wallet_hashes, parse_address, parse_agent_id, register_calldata, + selector, set_wallet_calldata, + }, + args::{Command, ConnectionMode, parse}, + host::{GuestInvocation, HostMetadata, ensure_host_abi}, +}; + +#[test] +fn parses_named_wallet_set_wallet() { + let args = vec![ + "set-wallet".to_string(), + "7".to_string(), + "alice".to_string(), + "--deadline-seconds".to_string(), + "120".to_string(), + ]; + + let command = parse(&args).expect("parse set-wallet"); + + match command { + Command::SetWallet(args) => { + assert_eq!(args.agent_id, "7"); + assert_eq!(args.wallet, "alice"); + assert_eq!(args.deadline_seconds, 120); + } + other => panic!("unexpected command: {other:?}"), + } +} + +#[test] +fn list_defaults_to_owner_mode() { + let args = vec!["list".to_string()]; + + let command = parse(&args).expect("parse list"); + + match command { + Command::List(args) => assert_eq!(args.connection, ConnectionMode::Owner), + other => panic!("unexpected command: {other:?}"), + } +} + +#[test] +fn parses_mutating_commands_with_registry_override() { + let registry = "0x2222222222222222222222222222222222222222"; + let set_uri = parse(&[ + "set-uri".to_string(), + "7".to_string(), + "https://agent.example/new.json".to_string(), + "--identity-registry".to_string(), + registry.to_string(), + ]) + .expect("parse set-uri override"); + match set_uri { + Command::SetUri(args) => { + assert_eq!(args.agent_id, "7"); + assert_eq!(args.identity_registry.as_deref(), Some(registry)); + assert_eq!(args.uri, "https://agent.example/new.json"); + } + other => panic!("unexpected command: {other:?}"), + } + + let unset_wallet = parse(&[ + "unset-wallet".to_string(), + "7".to_string(), + "--identity-registry".to_string(), + registry.to_string(), + ]) + .expect("parse unset-wallet override"); + match unset_wallet { + Command::UnsetWallet(args) => { + assert_eq!(args.agent_id, "7"); + assert_eq!(args.identity_registry.as_deref(), Some(registry)); + } + other => panic!("unexpected command: {other:?}"), + } +} + +#[test] +fn selectors_match_registry_abi() { + assert_eq!(selector("register()"), "0x1aa3a008"); + assert_eq!(selector("register(string)"), "0xf2c298be"); + assert_eq!( + selector("setAgentWallet(uint256,address,uint256,bytes)"), + "0x2d1ef5ae" + ); + assert_eq!(selector("getAgentWallet(uint256)"), "0x00339509"); +} + +#[test] +fn host_abi_requires_signature_and_logs_surface() { + let invocation = GuestInvocation { + args: vec!["support".to_string()], + host_api_version: 1, + metadata: HostMetadata { + app_id: "erc8004".to_string(), + app_version: "1.0.0".to_string(), + chain: "base".to_string(), + chain_id: 8453, + debug: false, + host_api_version: 1, + manifest_sha256: "sha256:manifest".to_string(), + now: 1_000, + wallet: "0x3333333333333333333333333333333333333333".to_string(), + wasm_sha256: "sha256:wasm".to_string(), + }, + output_mode: "default".to_string(), + }; + + ensure_host_abi(&invocation).expect("erc8004 uses the current host api"); +} + +#[test] +fn encodes_register_and_set_wallet_calldata() { + let register = register_calldata(Some("https://agent.example/agent.json")); + assert!(register.starts_with("0xf2c298be")); + + let wallet = parse_address("0x1111111111111111111111111111111111111111").expect("wallet"); + let data = set_wallet_calldata( + parse_agent_id("1").expect("agent id"), + wallet, + 42, + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1b", + ) + .expect("set wallet calldata"); + assert!(data.starts_with("0x2d1ef5ae")); +} + +#[test] +fn hashes_agent_wallet_typed_data() { + let registry = parse_address("0x8004A818BFB912233c491871b3d84c89A494BD9e").expect("registry"); + let wallet = parse_address("0x1111111111111111111111111111111111111111").expect("wallet"); + let owner = parse_address("0x2222222222222222222222222222222222222222").expect("owner"); + + let (domain_separator, struct_hash) = agent_wallet_hashes( + 11155111, + registry, + parse_agent_id("1").unwrap(), + wallet, + owner, + 300, + ); + + assert!(domain_separator.starts_with("0x")); + assert_eq!(domain_separator.len(), 66); + assert!(struct_hash.starts_with("0x")); + assert_eq!(struct_hash.len(), 66); + assert_eq!( + address_hex(wallet), + "0x1111111111111111111111111111111111111111" + ); +} diff --git a/beam-apps/apps/uniswap/README.md b/beam-apps/apps/uniswap/README.md index 0da5a32..9c5e738 100644 --- a/beam-apps/apps/uniswap/README.md +++ b/beam-apps/apps/uniswap/README.md @@ -42,12 +42,17 @@ beam x uniswap swap USDC ETH 100 --chain base --from alice ``` Beam shows the quote, any required approval, and the swap as a single plan. You -approve the final plan before Beam signs or submits anything. +approve the final plan before Beam signs or submits anything. Beam owns final +transaction pricing: the app may pass a route-specific gas-limit estimate, but +Uniswap API fee values are treated only as route metadata and Beam selects the +signed network fee within the approved cap. ## Options - `--min-receive ` sets the minimum acceptable output amount. -- `--max-gas ` rejects the plan if estimated gas exceeds the limit. +- `--max-gas ` rejects the plan if estimated gas limit exceeds the limit. + It is not a max network-fee cap; use Beam's `--max-network-fee-wei ` on + the app command to cap the per-step network fee. - `--slippage-bps ` sets max slippage in basis points. - `--recipient ` sends output to another wallet, ENS name, or EVM address. @@ -84,6 +89,10 @@ beam apps approvals show beam apps approvals approve --execute ``` +Prepared approval JSON includes Beam-owned network-fee caps for each executable +step. Old approvals created before fee caps existed require fresh approval +before execution. + `--no-prompt` fails closed for wallet-affecting swaps unless the command is preparing a continuation or executing an already-approved continuation. diff --git a/beam-apps/apps/uniswap/src/api.rs b/beam-apps/apps/uniswap/src/api.rs index f9e872f..bf41dc2 100644 --- a/beam-apps/apps/uniswap/src/api.rs +++ b/beam-apps/apps/uniswap/src/api.rs @@ -27,7 +27,7 @@ pub struct SwapResponse { pub struct UniswapTransaction { pub data: String, pub gas_limit: Option, - pub gas_price: Option, + pub gas_price_hint: Option, pub to: String, pub value: String, } @@ -196,7 +196,7 @@ fn parse_transaction(value: &Value) -> Option { Some(UniswapTransaction { data: first_string(value, &["data", "calldata", "input"])?, gas_limit: first_string(value, &["gasLimit", "gas"]), - gas_price: first_string(value, &["gasPrice", "maxFeePerGas"]), + gas_price_hint: first_string(value, &["gasPrice", "maxFeePerGas"]), to: first_string(value, &["to", "target"])?, value: first_string(value, &["value"]).unwrap_or_else(|| "0".to_string()), }) diff --git a/beam-apps/apps/uniswap/src/host.rs b/beam-apps/apps/uniswap/src/host.rs index 9419add..895d24a 100644 --- a/beam-apps/apps/uniswap/src/host.rs +++ b/beam-apps/apps/uniswap/src/host.rs @@ -207,8 +207,8 @@ struct StorageGetResponse { } pub fn ensure_host_abi(invocation: &GuestInvocation) -> Result<()> { - if invocation.host_api_version != HOST_API_VERSION - || invocation.metadata.host_api_version != HOST_API_VERSION + if invocation.host_api_version < HOST_API_VERSION + || invocation.metadata.host_api_version < HOST_API_VERSION { return Err(Error::InvalidHostResponse { reason: format!( diff --git a/beam-apps/apps/uniswap/src/lib.rs b/beam-apps/apps/uniswap/src/lib.rs index 0c8747e..a1d894c 100644 --- a/beam-apps/apps/uniswap/src/lib.rs +++ b/beam-apps/apps/uniswap/src/lib.rs @@ -180,16 +180,15 @@ fn run_swap(invocation: GuestInvocation) -> Result { raw: swap_value, }; debug(debug_enabled, "swap:transaction:parsed"); - if swap.transaction.gas_limit.is_none() || swap.transaction.gas_price.is_none() { + if swap.transaction.gas_limit.is_none() { debug(debug_enabled, "swap:gas:request"); - let (gas_limit, gas_price) = host::gas( + let (gas_limit, _) = host::gas( &chain, &swap.transaction.to, &swap.transaction.data, &swap.transaction.value, )?; swap.transaction.gas_limit = swap.transaction.gas_limit.or(gas_limit); - swap.transaction.gas_price = swap.transaction.gas_price.or(Some(gas_price)); debug(debug_enabled, "swap:gas:response"); } debug(debug_enabled, "swap:simulation:start"); diff --git a/beam-apps/apps/uniswap/src/plan.rs b/beam-apps/apps/uniswap/src/plan.rs index b716c47..b9efabb 100644 --- a/beam-apps/apps/uniswap/src/plan.rs +++ b/beam-apps/apps/uniswap/src/plan.rs @@ -158,6 +158,9 @@ fn swap_metadata(input: &SwapPlanInput, transaction: &UniswapTransaction) -> Val if let Some(gas_fee) = raw_string(&input.swap.raw, "gasFee") { metadata["gas_fee"] = json!(gas_fee); } + if let Some(gas_price_hint) = &transaction.gas_price_hint { + metadata["uniswap_gas_price_hint"] = json!(gas_price_hint); + } metadata } @@ -207,7 +210,6 @@ fn transaction_json(transaction: &UniswapTransaction) -> Value { json!({ "data": transaction.data, "gas_limit": transaction.gas_limit, - "gas_price": transaction.gas_price, "to": transaction.to, "value": transaction.value, }) diff --git a/beam-apps/apps/uniswap/src/tests.rs b/beam-apps/apps/uniswap/src/tests.rs index 47d6b7f..92f57e6 100644 --- a/beam-apps/apps/uniswap/src/tests.rs +++ b/beam-apps/apps/uniswap/src/tests.rs @@ -220,6 +220,15 @@ fn builds_approval_and_swap_action_plan() { Some("swap-request-1") ); assert_eq!(plan.steps[1].metadata["gas_fee"].as_str(), Some("123")); + assert_eq!( + plan.steps[1].metadata["uniswap_gas_price_hint"].as_str(), + Some("1") + ); + assert_eq!( + plan.steps[1].metadata["transaction"]["gas_limit"].as_str(), + Some("100000") + ); + assert_eq!(plan.steps[1].metadata["transaction"].get("gas_price"), None); assert!( plan.bindings .iter() @@ -258,7 +267,7 @@ fn transaction(data: &str) -> UniswapTransaction { UniswapTransaction { data: data.to_string(), gas_limit: Some("100000".to_string()), - gas_price: Some("1".to_string()), + gas_price_hint: Some("1".to_string()), to: "0x2222222222222222222222222222222222222222".to_string(), value: "0".to_string(), } diff --git a/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/icon.svg b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/icon.svg new file mode 100644 index 0000000..91a16e6 --- /dev/null +++ b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/manifest.json b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/manifest.json new file mode 100644 index 0000000..0b1ad8a --- /dev/null +++ b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/manifest.json @@ -0,0 +1,464 @@ +{ + "format_version": 1, + "id": "erc8004", + "display_name": "ERC-8004", + "version": "1.0.0", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "min_beam_version": "0.2.4", + "wasm": { + "sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "entrypoint": "beam_app_main" + }, + "catalog": { + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + } + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + } + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + } + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + } + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + } + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + } + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + } + ], + "permissions": { + "http": [ + { + "url": "https://*" + } + ], + "chains": [ + { + "chain": "*", + "operations": [ + "read", + "simulate" + ] + } + ], + "wallet": { + "read_balances": false, + "propose_transactions": true, + "erc20_approval": false, + "sign_typed_data": true + }, + "storage": { + "app_local": true + }, + "privacy": [] + }, + "host_api": { + "privacy_reserved": true + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:55f29f9deb74b97a05806a3a833a0797fe7e54af7de7637c40732047e1bd49ff" + } +} diff --git a/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/manifest.json.sig b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/manifest.json.sig new file mode 100644 index 0000000..33f51cd --- /dev/null +++ b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/manifest.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" +} diff --git a/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/module.wasm b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/module.wasm new file mode 100644 index 0000000..6e620ea Binary files /dev/null and b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/version.json.sig b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/version.json.sig new file mode 100644 index 0000000..ecd262c --- /dev/null +++ b/beam-apps/fixtures/broad-wildcard/apps/erc8004/1.0.0/version.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" +} diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json index 123dc1e..1113f05 100644 --- a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json +++ b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.2.1", "wasm": { - "sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "entrypoint": "beam_app_main" }, "catalog": { @@ -190,10 +190,81 @@ ], "chains": [ { - "chain": "*", + "chain": "ethereum", "operations": [ "read", - "simulate" + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "base", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "polygon", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "bnb", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "arbitrum", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "sepolia", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" ] } ], @@ -219,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:e27bed5240e9eeb775f4830f1e46149e514057c6e4b4ca3574f027fa12d9daa1" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } } diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json.sig b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json.sig index 53eeada..53f61c1 100644 --- a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json.sig +++ b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/module.wasm b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/module.wasm index 4d88a6a..1d43407 100644 Binary files a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/module.wasm and b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/module.wasm differ diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/version.json.sig b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/version.json.sig index 250c2e2..d1cb93b 100644 --- a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/version.json.sig +++ b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.2/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } diff --git a/beam-apps/fixtures/broad-wildcard/catalog/apps.json b/beam-apps/fixtures/broad-wildcard/catalog/apps.json index dbba548..b03a654 100644 --- a/beam-apps/fixtures/broad-wildcard/catalog/apps.json +++ b/beam-apps/fixtures/broad-wildcard/catalog/apps.json @@ -2,6 +2,48 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "install_command": "beam apps install erc8004", + "pinned_install_command": "beam apps install erc8004 --version 1.0.0", + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "version": { + "version": "1.0.0", + "min_beam_version": "0.2.4" + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, { "id": "uniswap", "display_name": "Uniswap", @@ -104,6 +146,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } } diff --git a/beam-apps/fixtures/broad-wildcard/catalog/apps.json.sig b/beam-apps/fixtures/broad-wildcard/catalog/apps.json.sig index 4d6ed71..d6d261d 100644 --- a/beam-apps/fixtures/broad-wildcard/catalog/apps.json.sig +++ b/beam-apps/fixtures/broad-wildcard/catalog/apps.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } diff --git a/beam-apps/fixtures/broad-wildcard/catalog/apps/erc8004.json b/beam-apps/fixtures/broad-wildcard/catalog/apps/erc8004.json new file mode 100644 index 0000000..7356f71 --- /dev/null +++ b/beam-apps/fixtures/broad-wildcard/catalog/apps/erc8004.json @@ -0,0 +1,485 @@ +{ + "format_version": 1, + "generated_at": "2026-05-26T00:00:00Z", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "app": { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "install_commands": { + "latest": "beam apps install erc8004", + "pinned": "beam apps install erc8004 --version 1.0.0", + "dry_run": "beam apps install erc8004 --dry-run" + }, + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "permission_summary": { + "http": [ + "https://*" + ], + "wallet": [ + "propose transactions" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ], + "storage": [ + "app-local" + ], + "privacy": [] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + } + ], + "readme_markdown": "# ERC-8004 Beam App\n\nThe ERC-8004 app manages identity-registry agents through Beam's generic app\nhost. It keeps registry defaults and overrides in app space rather than as a\nnative Beam command.\n\n```text\nbeam x erc8004 support\nbeam x erc8004 config show\nbeam x erc8004 config set --identity-registry
\nbeam x erc8004 register [--uri |--empty-uri] [--identity-registry
]\nbeam x erc8004 show [--fetch-uri] [--identity-registry
]\nbeam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--identity-registry
]\nbeam x erc8004 set-uri [--identity-registry
]\nbeam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]\nbeam x erc8004 unset-wallet [--identity-registry
]\n```\n\nDefault ERC-8004 identity registry addresses are manifest-scoped. Custom\nregistry addresses come from app-local config or an explicit\n`--identity-registry` flag and are included as invocation-scoped contract rules\nin host calls and action plans.\n\n`list` uses `eth_getLogs` through the Beam host. The host enforces a bounded\nblock range and the app defaults to the active wallet with owner filtering, so\nit does not scan from genesis unless the user passes a broad explicit range.\n\n`set-wallet` resolves the wallet argument through Beam and requests an EIP-712\ntyped-data signature from the host. The app receives only the signature and\ndigest, never raw private keys.\n", + "manifest_summary": { + "format_version": 1, + "min_beam_version": "0.2.4", + "wasm_entrypoint": "beam_app_main" + }, + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4" + } + ], + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" + } +} diff --git a/beam-apps/fixtures/broad-wildcard/catalog/apps/erc8004.json.sig b/beam-apps/fixtures/broad-wildcard/catalog/apps/erc8004.json.sig new file mode 100644 index 0000000..0c9c285 --- /dev/null +++ b/beam-apps/fixtures/broad-wildcard/catalog/apps/erc8004.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" +} diff --git a/beam-apps/fixtures/broad-wildcard/index.json b/beam-apps/fixtures/broad-wildcard/index.json index b3923c1..6ab4c79 100644 --- a/beam-apps/fixtures/broad-wildcard/index.json +++ b/beam-apps/fixtures/broad-wildcard/index.json @@ -2,6 +2,27 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4", + "manifest_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/manifest.json", + "manifest_sha256": "sha256:ddbfd3a3e4c03eb65003a2f44c231d85d22e4a2b9de9173b33894202cb2c3c3a", + "module_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/module.wasm", + "module_sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" + } + } + ] + }, { "id": "uniswap", "name": "Uniswap", @@ -12,13 +33,13 @@ "version": "1.0.2", "min_beam_version": "0.2.1", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/manifest.json", - "manifest_sha256": "sha256:b3bee997c062dbf20de57c4176a010b3578bf4a5e7c8a57017733bd5a95e2b73", + "manifest_sha256": "sha256:36c9fc35b9d855622201146244eeb165e97592cbd333296d7e3f1454f37be545", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/module.wasm", - "module_sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "module_sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } } ] @@ -27,6 +48,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } } diff --git a/beam-apps/fixtures/broad-wildcard/index.json.sig b/beam-apps/fixtures/broad-wildcard/index.json.sig index 4193503..30b012f 100644 --- a/beam-apps/fixtures/broad-wildcard/index.json.sig +++ b/beam-apps/fixtures/broad-wildcard/index.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } diff --git a/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/icon.svg b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/icon.svg new file mode 100644 index 0000000..91a16e6 --- /dev/null +++ b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/manifest.json b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/manifest.json new file mode 100644 index 0000000..f69e711 --- /dev/null +++ b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/manifest.json @@ -0,0 +1,480 @@ +{ + "format_version": 1, + "id": "erc8004", + "display_name": "ERC-8004", + "version": "1.0.0", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "min_beam_version": "0.2.4", + "wasm": { + "sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "entrypoint": "beam_app_main" + }, + "catalog": { + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + } + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + } + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + } + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + } + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + } + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + } + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + } + ], + "permissions": { + "http": [ + { + "url": "https://*" + } + ], + "chains": [ + { + "chain": "*", + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ], + "contracts": [ + "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "0x8004A818BFB912233c491871b3d84c89A494BD9e" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ] + } + ], + "wallet": { + "read_balances": false, + "propose_transactions": true, + "erc20_approval": false, + "sign_typed_data": true + }, + "storage": { + "app_local": true + }, + "privacy": [] + }, + "host_api": { + "privacy_reserved": true + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" + } +} diff --git a/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/manifest.json.sig b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/manifest.json.sig new file mode 100644 index 0000000..33f51cd --- /dev/null +++ b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/manifest.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" +} diff --git a/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/module.wasm b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/module.wasm new file mode 100644 index 0000000..6e620ea Binary files /dev/null and b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/version.json.sig b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/version.json.sig new file mode 100644 index 0000000..ecd262c --- /dev/null +++ b/beam-apps/fixtures/invalid-digest/apps/erc8004/1.0.0/version.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" +} diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json index 4b4bf56..1113f05 100644 --- a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json +++ b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.2.1", "wasm": { - "sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "entrypoint": "beam_app_main" }, "catalog": { @@ -290,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } } diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json.sig b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json.sig index 53eeada..53f61c1 100644 --- a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json.sig +++ b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/module.wasm b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/module.wasm index 4d88a6a..1d43407 100644 Binary files a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/module.wasm and b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/module.wasm differ diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/version.json.sig b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/version.json.sig index 250c2e2..d1cb93b 100644 --- a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/version.json.sig +++ b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.2/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } diff --git a/beam-apps/fixtures/invalid-digest/catalog/apps.json b/beam-apps/fixtures/invalid-digest/catalog/apps.json index dbba548..b03a654 100644 --- a/beam-apps/fixtures/invalid-digest/catalog/apps.json +++ b/beam-apps/fixtures/invalid-digest/catalog/apps.json @@ -2,6 +2,48 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "install_command": "beam apps install erc8004", + "pinned_install_command": "beam apps install erc8004 --version 1.0.0", + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "version": { + "version": "1.0.0", + "min_beam_version": "0.2.4" + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, { "id": "uniswap", "display_name": "Uniswap", @@ -104,6 +146,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } } diff --git a/beam-apps/fixtures/invalid-digest/catalog/apps.json.sig b/beam-apps/fixtures/invalid-digest/catalog/apps.json.sig index 4d6ed71..d6d261d 100644 --- a/beam-apps/fixtures/invalid-digest/catalog/apps.json.sig +++ b/beam-apps/fixtures/invalid-digest/catalog/apps.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } diff --git a/beam-apps/fixtures/invalid-digest/catalog/apps/erc8004.json b/beam-apps/fixtures/invalid-digest/catalog/apps/erc8004.json new file mode 100644 index 0000000..7356f71 --- /dev/null +++ b/beam-apps/fixtures/invalid-digest/catalog/apps/erc8004.json @@ -0,0 +1,485 @@ +{ + "format_version": 1, + "generated_at": "2026-05-26T00:00:00Z", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "app": { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "install_commands": { + "latest": "beam apps install erc8004", + "pinned": "beam apps install erc8004 --version 1.0.0", + "dry_run": "beam apps install erc8004 --dry-run" + }, + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "permission_summary": { + "http": [ + "https://*" + ], + "wallet": [ + "propose transactions" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ], + "storage": [ + "app-local" + ], + "privacy": [] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + } + ], + "readme_markdown": "# ERC-8004 Beam App\n\nThe ERC-8004 app manages identity-registry agents through Beam's generic app\nhost. It keeps registry defaults and overrides in app space rather than as a\nnative Beam command.\n\n```text\nbeam x erc8004 support\nbeam x erc8004 config show\nbeam x erc8004 config set --identity-registry
\nbeam x erc8004 register [--uri |--empty-uri] [--identity-registry
]\nbeam x erc8004 show [--fetch-uri] [--identity-registry
]\nbeam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--identity-registry
]\nbeam x erc8004 set-uri [--identity-registry
]\nbeam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]\nbeam x erc8004 unset-wallet [--identity-registry
]\n```\n\nDefault ERC-8004 identity registry addresses are manifest-scoped. Custom\nregistry addresses come from app-local config or an explicit\n`--identity-registry` flag and are included as invocation-scoped contract rules\nin host calls and action plans.\n\n`list` uses `eth_getLogs` through the Beam host. The host enforces a bounded\nblock range and the app defaults to the active wallet with owner filtering, so\nit does not scan from genesis unless the user passes a broad explicit range.\n\n`set-wallet` resolves the wallet argument through Beam and requests an EIP-712\ntyped-data signature from the host. The app receives only the signature and\ndigest, never raw private keys.\n", + "manifest_summary": { + "format_version": 1, + "min_beam_version": "0.2.4", + "wasm_entrypoint": "beam_app_main" + }, + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4" + } + ], + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" + } +} diff --git a/beam-apps/fixtures/invalid-digest/catalog/apps/erc8004.json.sig b/beam-apps/fixtures/invalid-digest/catalog/apps/erc8004.json.sig new file mode 100644 index 0000000..0c9c285 --- /dev/null +++ b/beam-apps/fixtures/invalid-digest/catalog/apps/erc8004.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" +} diff --git a/beam-apps/fixtures/invalid-digest/index.json b/beam-apps/fixtures/invalid-digest/index.json index ff3fde0..1f9f1c0 100644 --- a/beam-apps/fixtures/invalid-digest/index.json +++ b/beam-apps/fixtures/invalid-digest/index.json @@ -2,6 +2,27 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4", + "manifest_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/manifest.json", + "manifest_sha256": "sha256:ddbfd3a3e4c03eb65003a2f44c231d85d22e4a2b9de9173b33894202cb2c3c3a", + "module_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/module.wasm", + "module_sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" + } + } + ] + }, { "id": "uniswap", "name": "Uniswap", @@ -12,13 +33,13 @@ "version": "1.0.2", "min_beam_version": "0.2.1", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/manifest.json", - "manifest_sha256": "sha256:b3bee997c062dbf20de57c4176a010b3578bf4a5e7c8a57017733bd5a95e2b73", + "manifest_sha256": "sha256:36c9fc35b9d855622201146244eeb165e97592cbd333296d7e3f1454f37be545", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/module.wasm", - "module_sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "module_sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } } ] @@ -27,6 +48,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:1d37a1f8f1729015d06f67214042aed05298a44e21bc07c63c266cb19e3f421d" + "value": "sha256:325441bbd9c9183309488eee5df775327e36015e356ab793efb9202ebcd1a595" } } diff --git a/beam-apps/fixtures/invalid-digest/index.json.sig b/beam-apps/fixtures/invalid-digest/index.json.sig index 4193503..30b012f 100644 --- a/beam-apps/fixtures/invalid-digest/index.json.sig +++ b/beam-apps/fixtures/invalid-digest/index.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } diff --git a/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/icon.svg b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/icon.svg new file mode 100644 index 0000000..91a16e6 --- /dev/null +++ b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/manifest.json b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/manifest.json new file mode 100644 index 0000000..a88ae5a --- /dev/null +++ b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/manifest.json @@ -0,0 +1,466 @@ +{ + "format_version": 1, + "id": "erc8004", + "display_name": "ERC-8004", + "version": "1.0.0", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "min_beam_version": "0.2.4", + "wasm": { + "sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "entrypoint": "beam_app_main" + }, + "catalog": { + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + } + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + } + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + } + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + } + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + } + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + } + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + } + ], + "permissions": { + "http": [ + { + "url": "https://*" + } + ], + "chains": [ + { + "chain": "base", + "operations": [ + "read" + ], + "selectors": [ + "not-a-selector" + ] + } + ], + "wallet": { + "read_balances": false, + "propose_transactions": true, + "erc20_approval": false, + "sign_typed_data": true + }, + "storage": { + "app_local": true + }, + "privacy": [] + }, + "host_api": { + "privacy_reserved": true + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a0b37dca027eccfcfbbe37d7210bf657bd1e2e9918ab8e1d1a722d09facaae46" + } +} diff --git a/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/manifest.json.sig b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/manifest.json.sig new file mode 100644 index 0000000..33f51cd --- /dev/null +++ b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/manifest.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" +} diff --git a/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/module.wasm b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/module.wasm new file mode 100644 index 0000000..6e620ea Binary files /dev/null and b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/version.json.sig b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/version.json.sig new file mode 100644 index 0000000..ecd262c --- /dev/null +++ b/beam-apps/fixtures/malformed-permissions/apps/erc8004/1.0.0/version.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" +} diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json index 200fe0c..1113f05 100644 --- a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json +++ b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.2.1", "wasm": { - "sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "entrypoint": "beam_app_main" }, "catalog": { @@ -189,13 +189,82 @@ } ], "chains": [ + { + "chain": "ethereum", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, { "chain": "base", "operations": [ - "read" + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "polygon", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "bnb", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "arbitrum", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" + ], + "selectors": [ + "0x095ea7b3", + "*" + ] + }, + { + "chain": "sepolia", + "operations": [ + "read", + "simulate", + "send-transaction", + "erc20-approval" ], "selectors": [ - "not-a-selector" + "0x095ea7b3", + "*" ] } ], @@ -221,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:8e136bd9b04f736be0593506305b42957f53f8ee94b21857275645315d52dc32" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } } diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json.sig b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json.sig index 53eeada..53f61c1 100644 --- a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json.sig +++ b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/module.wasm b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/module.wasm index 4d88a6a..1d43407 100644 Binary files a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/module.wasm and b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/module.wasm differ diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/version.json.sig b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/version.json.sig index 250c2e2..d1cb93b 100644 --- a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/version.json.sig +++ b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.2/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } diff --git a/beam-apps/fixtures/malformed-permissions/catalog/apps.json b/beam-apps/fixtures/malformed-permissions/catalog/apps.json index dbba548..b03a654 100644 --- a/beam-apps/fixtures/malformed-permissions/catalog/apps.json +++ b/beam-apps/fixtures/malformed-permissions/catalog/apps.json @@ -2,6 +2,48 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "install_command": "beam apps install erc8004", + "pinned_install_command": "beam apps install erc8004 --version 1.0.0", + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "version": { + "version": "1.0.0", + "min_beam_version": "0.2.4" + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, { "id": "uniswap", "display_name": "Uniswap", @@ -104,6 +146,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } } diff --git a/beam-apps/fixtures/malformed-permissions/catalog/apps.json.sig b/beam-apps/fixtures/malformed-permissions/catalog/apps.json.sig index 4d6ed71..d6d261d 100644 --- a/beam-apps/fixtures/malformed-permissions/catalog/apps.json.sig +++ b/beam-apps/fixtures/malformed-permissions/catalog/apps.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } diff --git a/beam-apps/fixtures/malformed-permissions/catalog/apps/erc8004.json b/beam-apps/fixtures/malformed-permissions/catalog/apps/erc8004.json new file mode 100644 index 0000000..7356f71 --- /dev/null +++ b/beam-apps/fixtures/malformed-permissions/catalog/apps/erc8004.json @@ -0,0 +1,485 @@ +{ + "format_version": 1, + "generated_at": "2026-05-26T00:00:00Z", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "app": { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "install_commands": { + "latest": "beam apps install erc8004", + "pinned": "beam apps install erc8004 --version 1.0.0", + "dry_run": "beam apps install erc8004 --dry-run" + }, + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "permission_summary": { + "http": [ + "https://*" + ], + "wallet": [ + "propose transactions" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ], + "storage": [ + "app-local" + ], + "privacy": [] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + } + ], + "readme_markdown": "# ERC-8004 Beam App\n\nThe ERC-8004 app manages identity-registry agents through Beam's generic app\nhost. It keeps registry defaults and overrides in app space rather than as a\nnative Beam command.\n\n```text\nbeam x erc8004 support\nbeam x erc8004 config show\nbeam x erc8004 config set --identity-registry
\nbeam x erc8004 register [--uri |--empty-uri] [--identity-registry
]\nbeam x erc8004 show [--fetch-uri] [--identity-registry
]\nbeam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--identity-registry
]\nbeam x erc8004 set-uri [--identity-registry
]\nbeam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]\nbeam x erc8004 unset-wallet [--identity-registry
]\n```\n\nDefault ERC-8004 identity registry addresses are manifest-scoped. Custom\nregistry addresses come from app-local config or an explicit\n`--identity-registry` flag and are included as invocation-scoped contract rules\nin host calls and action plans.\n\n`list` uses `eth_getLogs` through the Beam host. The host enforces a bounded\nblock range and the app defaults to the active wallet with owner filtering, so\nit does not scan from genesis unless the user passes a broad explicit range.\n\n`set-wallet` resolves the wallet argument through Beam and requests an EIP-712\ntyped-data signature from the host. The app receives only the signature and\ndigest, never raw private keys.\n", + "manifest_summary": { + "format_version": 1, + "min_beam_version": "0.2.4", + "wasm_entrypoint": "beam_app_main" + }, + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4" + } + ], + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" + } +} diff --git a/beam-apps/fixtures/malformed-permissions/catalog/apps/erc8004.json.sig b/beam-apps/fixtures/malformed-permissions/catalog/apps/erc8004.json.sig new file mode 100644 index 0000000..0c9c285 --- /dev/null +++ b/beam-apps/fixtures/malformed-permissions/catalog/apps/erc8004.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" +} diff --git a/beam-apps/fixtures/malformed-permissions/index.json b/beam-apps/fixtures/malformed-permissions/index.json index b3923c1..6ab4c79 100644 --- a/beam-apps/fixtures/malformed-permissions/index.json +++ b/beam-apps/fixtures/malformed-permissions/index.json @@ -2,6 +2,27 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4", + "manifest_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/manifest.json", + "manifest_sha256": "sha256:ddbfd3a3e4c03eb65003a2f44c231d85d22e4a2b9de9173b33894202cb2c3c3a", + "module_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/module.wasm", + "module_sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" + } + } + ] + }, { "id": "uniswap", "name": "Uniswap", @@ -12,13 +33,13 @@ "version": "1.0.2", "min_beam_version": "0.2.1", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/manifest.json", - "manifest_sha256": "sha256:b3bee997c062dbf20de57c4176a010b3578bf4a5e7c8a57017733bd5a95e2b73", + "manifest_sha256": "sha256:36c9fc35b9d855622201146244eeb165e97592cbd333296d7e3f1454f37be545", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/module.wasm", - "module_sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "module_sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } } ] @@ -27,6 +48,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } } diff --git a/beam-apps/fixtures/malformed-permissions/index.json.sig b/beam-apps/fixtures/malformed-permissions/index.json.sig index 4193503..30b012f 100644 --- a/beam-apps/fixtures/malformed-permissions/index.json.sig +++ b/beam-apps/fixtures/malformed-permissions/index.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } diff --git a/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/icon.svg b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/icon.svg new file mode 100644 index 0000000..91a16e6 --- /dev/null +++ b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/manifest.json b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/manifest.json new file mode 100644 index 0000000..89fae03 --- /dev/null +++ b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/manifest.json @@ -0,0 +1,79 @@ +{ + "format_version": 1, + "id": "erc8004", + "display_name": "ERC-8004", + "version": "1.0.0", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "min_beam_version": "0.2.4", + "wasm": { + "sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "entrypoint": "beam_app_main" + }, + "catalog": { + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ] + }, + "permissions": { + "http": [ + { + "url": "https://*" + } + ], + "chains": [ + { + "chain": "*", + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ], + "contracts": [ + "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "0x8004A818BFB912233c491871b3d84c89A494BD9e" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ] + } + ], + "wallet": { + "read_balances": false, + "propose_transactions": true, + "erc20_approval": false, + "sign_typed_data": true + }, + "storage": { + "app_local": true + }, + "privacy": [] + }, + "host_api": { + "privacy_reserved": true + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:c4e03f2cec7deacdd834d09aa0fc3708540cc261d4748f7abb62db1f7ce5b998" + } +} diff --git a/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/manifest.json.sig b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/manifest.json.sig new file mode 100644 index 0000000..33f51cd --- /dev/null +++ b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/manifest.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" +} diff --git a/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/module.wasm b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/module.wasm new file mode 100644 index 0000000..6e620ea Binary files /dev/null and b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/version.json.sig b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/version.json.sig new file mode 100644 index 0000000..ecd262c --- /dev/null +++ b/beam-apps/fixtures/missing-fields/apps/erc8004/1.0.0/version.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" +} diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json index 475d76c..1113f05 100644 --- a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json +++ b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.2.1", "wasm": { - "sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "entrypoint": "beam_app_main" }, "catalog": { @@ -21,6 +21,167 @@ "App storage" ] }, + "commands": [ + { + "name": "swap", + "about": "Fetch a quote and prepare an exact-approval Uniswap swap", + "usage": "swap [--min-receive ] [--max-gas ] [--slippage-bps ] [--recipient ] [--deadline-seconds ] [--unlimited-approval]", + "sensitive_args": [], + "input_schema": { + "type": "object", + "required": [ + "sell_token", + "buy_token", + "amount" + ], + "properties": { + "sell_token": { + "type": "string" + }, + "buy_token": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "min_receive": { + "type": "string" + }, + "max_gas": { + "type": "string" + }, + "slippage_bps": { + "type": "integer" + }, + "recipient": { + "type": "string" + }, + "deadline_seconds": { + "type": "integer" + }, + "unlimited_approval": { + "type": "boolean" + } + } + }, + "output_schema": { + "type": "object", + "required": [ + "state", + "steps" + ], + "properties": { + "state": { + "enum": [ + "prepared", + "pending", + "confirmed", + "dropped" + ] + }, + "steps": { + "type": "array" + }, + "transaction_hash": { + "type": "string" + } + } + }, + "docs": { + "summary": "Fetch a quote and prepare an exact-approval Uniswap swap", + "invocation": "beam x uniswap swap [--min-receive ] [--max-gas ] [--slippage-bps ] [--recipient ] [--deadline-seconds ] [--unlimited-approval]", + "arguments": [ + { + "name": "sell-token", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Token to spend, provided as a symbol, Beam token alias, or address." + }, + { + "name": "buy-token", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Token to receive, provided as a symbol, Beam token alias, or address." + }, + { + "name": "amount", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Sell amount in human token units." + } + ], + "options": [ + { + "name": "--min-receive", + "value_name": "amount", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Minimum acceptable output amount; floors slippage." + }, + { + "name": "--max-gas", + "value_name": "wei", + "kind": "wei", + "required": false, + "sensitive": false, + "description": "Reject the plan if the estimated gas exceeds this value." + }, + { + "name": "--slippage-bps", + "value_name": "bps", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Maximum slippage in basis points." + }, + { + "name": "--recipient", + "value_name": "wallet-or-address", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Send output to another Beam wallet name, ENS name, or EVM address." + }, + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Quote and transaction deadline window." + }, + { + "name": "--unlimited-approval", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Request an unlimited ERC-20 approval instead of Beam's default exact approval." + } + ], + "examples": [ + { + "title": "Swap on Base", + "command": "beam x uniswap swap USDC ETH 100 --chain base --from alice", + "description": "Prepare and approve a public swap from the alice wallet on Base." + }, + { + "title": "Prepare for an agent", + "command": "beam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json", + "description": "Create a continuation for non-interactive review and explicit approval." + } + ], + "output_notes": [ + "Returns structured output with state prepared, pending, confirmed, or dropped.", + "Includes the typed steps Beam executed and a transaction_hash once broadcast." + ] + } + } + ], "permissions": { "http": [ { @@ -129,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:ce538d2a143e48a1d5dd7bd36896f3727f4148e589ddca7d601e6d5b4b5d5f9b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } } diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json.sig b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json.sig index 53eeada..53f61c1 100644 --- a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json.sig +++ b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/module.wasm b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/module.wasm index 4d88a6a..1d43407 100644 Binary files a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/module.wasm and b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/module.wasm differ diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/version.json.sig b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/version.json.sig index 250c2e2..d1cb93b 100644 --- a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/version.json.sig +++ b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.2/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } diff --git a/beam-apps/fixtures/missing-fields/catalog/apps.json b/beam-apps/fixtures/missing-fields/catalog/apps.json index dbba548..b03a654 100644 --- a/beam-apps/fixtures/missing-fields/catalog/apps.json +++ b/beam-apps/fixtures/missing-fields/catalog/apps.json @@ -2,6 +2,48 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "install_command": "beam apps install erc8004", + "pinned_install_command": "beam apps install erc8004 --version 1.0.0", + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "version": { + "version": "1.0.0", + "min_beam_version": "0.2.4" + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, { "id": "uniswap", "display_name": "Uniswap", @@ -104,6 +146,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } } diff --git a/beam-apps/fixtures/missing-fields/catalog/apps.json.sig b/beam-apps/fixtures/missing-fields/catalog/apps.json.sig index 4d6ed71..d6d261d 100644 --- a/beam-apps/fixtures/missing-fields/catalog/apps.json.sig +++ b/beam-apps/fixtures/missing-fields/catalog/apps.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } diff --git a/beam-apps/fixtures/missing-fields/catalog/apps/erc8004.json b/beam-apps/fixtures/missing-fields/catalog/apps/erc8004.json new file mode 100644 index 0000000..7356f71 --- /dev/null +++ b/beam-apps/fixtures/missing-fields/catalog/apps/erc8004.json @@ -0,0 +1,485 @@ +{ + "format_version": 1, + "generated_at": "2026-05-26T00:00:00Z", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "app": { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "install_commands": { + "latest": "beam apps install erc8004", + "pinned": "beam apps install erc8004 --version 1.0.0", + "dry_run": "beam apps install erc8004 --dry-run" + }, + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "permission_summary": { + "http": [ + "https://*" + ], + "wallet": [ + "propose transactions" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ], + "storage": [ + "app-local" + ], + "privacy": [] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + } + ], + "readme_markdown": "# ERC-8004 Beam App\n\nThe ERC-8004 app manages identity-registry agents through Beam's generic app\nhost. It keeps registry defaults and overrides in app space rather than as a\nnative Beam command.\n\n```text\nbeam x erc8004 support\nbeam x erc8004 config show\nbeam x erc8004 config set --identity-registry
\nbeam x erc8004 register [--uri |--empty-uri] [--identity-registry
]\nbeam x erc8004 show [--fetch-uri] [--identity-registry
]\nbeam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--identity-registry
]\nbeam x erc8004 set-uri [--identity-registry
]\nbeam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]\nbeam x erc8004 unset-wallet [--identity-registry
]\n```\n\nDefault ERC-8004 identity registry addresses are manifest-scoped. Custom\nregistry addresses come from app-local config or an explicit\n`--identity-registry` flag and are included as invocation-scoped contract rules\nin host calls and action plans.\n\n`list` uses `eth_getLogs` through the Beam host. The host enforces a bounded\nblock range and the app defaults to the active wallet with owner filtering, so\nit does not scan from genesis unless the user passes a broad explicit range.\n\n`set-wallet` resolves the wallet argument through Beam and requests an EIP-712\ntyped-data signature from the host. The app receives only the signature and\ndigest, never raw private keys.\n", + "manifest_summary": { + "format_version": 1, + "min_beam_version": "0.2.4", + "wasm_entrypoint": "beam_app_main" + }, + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4" + } + ], + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" + } +} diff --git a/beam-apps/fixtures/missing-fields/catalog/apps/erc8004.json.sig b/beam-apps/fixtures/missing-fields/catalog/apps/erc8004.json.sig new file mode 100644 index 0000000..0c9c285 --- /dev/null +++ b/beam-apps/fixtures/missing-fields/catalog/apps/erc8004.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" +} diff --git a/beam-apps/fixtures/missing-fields/index.json b/beam-apps/fixtures/missing-fields/index.json index b3923c1..6ab4c79 100644 --- a/beam-apps/fixtures/missing-fields/index.json +++ b/beam-apps/fixtures/missing-fields/index.json @@ -2,6 +2,27 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4", + "manifest_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/manifest.json", + "manifest_sha256": "sha256:ddbfd3a3e4c03eb65003a2f44c231d85d22e4a2b9de9173b33894202cb2c3c3a", + "module_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/module.wasm", + "module_sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" + } + } + ] + }, { "id": "uniswap", "name": "Uniswap", @@ -12,13 +33,13 @@ "version": "1.0.2", "min_beam_version": "0.2.1", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/manifest.json", - "manifest_sha256": "sha256:b3bee997c062dbf20de57c4176a010b3578bf4a5e7c8a57017733bd5a95e2b73", + "manifest_sha256": "sha256:36c9fc35b9d855622201146244eeb165e97592cbd333296d7e3f1454f37be545", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/module.wasm", - "module_sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "module_sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } } ] @@ -27,6 +48,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } } diff --git a/beam-apps/fixtures/missing-fields/index.json.sig b/beam-apps/fixtures/missing-fields/index.json.sig index 4193503..30b012f 100644 --- a/beam-apps/fixtures/missing-fields/index.json.sig +++ b/beam-apps/fixtures/missing-fields/index.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } diff --git a/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/icon.svg b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/icon.svg new file mode 100644 index 0000000..91a16e6 --- /dev/null +++ b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/manifest.json b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/manifest.json new file mode 100644 index 0000000..544f074 --- /dev/null +++ b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/manifest.json @@ -0,0 +1,480 @@ +{ + "format_version": 1, + "id": "erc8004", + "display_name": "ERC-8004", + "version": "1.0.0", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "min_beam_version": "999.0.0", + "wasm": { + "sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "entrypoint": "beam_app_main" + }, + "catalog": { + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + } + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + } + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + } + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + } + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + } + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + } + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + } + ], + "permissions": { + "http": [ + { + "url": "https://*" + } + ], + "chains": [ + { + "chain": "*", + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ], + "contracts": [ + "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "0x8004A818BFB912233c491871b3d84c89A494BD9e" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ] + } + ], + "wallet": { + "read_balances": false, + "propose_transactions": true, + "erc20_approval": false, + "sign_typed_data": true + }, + "storage": { + "app_local": true + }, + "privacy": [] + }, + "host_api": { + "privacy_reserved": true + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:f28094e7e85f0d592e7994f190f62fdfccd6e116700205f338c3615bf24bea88" + } +} diff --git a/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/manifest.json.sig b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/manifest.json.sig new file mode 100644 index 0000000..33f51cd --- /dev/null +++ b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/manifest.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" +} diff --git a/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/module.wasm b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/module.wasm new file mode 100644 index 0000000..6e620ea Binary files /dev/null and b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/version.json.sig b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/version.json.sig new file mode 100644 index 0000000..ecd262c --- /dev/null +++ b/beam-apps/fixtures/unsupported-beam/apps/erc8004/1.0.0/version.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" +} diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json index b418ab9..1113f05 100644 --- a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json +++ b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json @@ -5,9 +5,9 @@ "version": "1.0.2", "publisher": "Payy", "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", - "min_beam_version": "999.0.0", + "min_beam_version": "0.2.1", "wasm": { - "sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "entrypoint": "beam_app_main" }, "catalog": { @@ -290,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:29e99d1f0d955b6002cc80bc9f4e3c4d7fb80d62da8e153b582ba18529d0a0a0" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } } diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json.sig b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json.sig index 53eeada..53f61c1 100644 --- a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json.sig +++ b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/module.wasm b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/module.wasm index 4d88a6a..1d43407 100644 Binary files a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/module.wasm and b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/module.wasm differ diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/version.json.sig b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/version.json.sig index 250c2e2..d1cb93b 100644 --- a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/version.json.sig +++ b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.2/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } diff --git a/beam-apps/fixtures/unsupported-beam/catalog/apps.json b/beam-apps/fixtures/unsupported-beam/catalog/apps.json index dbba548..b03a654 100644 --- a/beam-apps/fixtures/unsupported-beam/catalog/apps.json +++ b/beam-apps/fixtures/unsupported-beam/catalog/apps.json @@ -2,6 +2,48 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "install_command": "beam apps install erc8004", + "pinned_install_command": "beam apps install erc8004 --version 1.0.0", + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "version": { + "version": "1.0.0", + "min_beam_version": "0.2.4" + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, { "id": "uniswap", "display_name": "Uniswap", @@ -104,6 +146,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } } diff --git a/beam-apps/fixtures/unsupported-beam/catalog/apps.json.sig b/beam-apps/fixtures/unsupported-beam/catalog/apps.json.sig index 4d6ed71..d6d261d 100644 --- a/beam-apps/fixtures/unsupported-beam/catalog/apps.json.sig +++ b/beam-apps/fixtures/unsupported-beam/catalog/apps.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } diff --git a/beam-apps/fixtures/unsupported-beam/catalog/apps/erc8004.json b/beam-apps/fixtures/unsupported-beam/catalog/apps/erc8004.json new file mode 100644 index 0000000..7356f71 --- /dev/null +++ b/beam-apps/fixtures/unsupported-beam/catalog/apps/erc8004.json @@ -0,0 +1,485 @@ +{ + "format_version": 1, + "generated_at": "2026-05-26T00:00:00Z", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "app": { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "install_commands": { + "latest": "beam apps install erc8004", + "pinned": "beam apps install erc8004 --version 1.0.0", + "dry_run": "beam apps install erc8004 --dry-run" + }, + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "permission_summary": { + "http": [ + "https://*" + ], + "wallet": [ + "propose transactions" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ], + "storage": [ + "app-local" + ], + "privacy": [] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + } + ], + "readme_markdown": "# ERC-8004 Beam App\n\nThe ERC-8004 app manages identity-registry agents through Beam's generic app\nhost. It keeps registry defaults and overrides in app space rather than as a\nnative Beam command.\n\n```text\nbeam x erc8004 support\nbeam x erc8004 config show\nbeam x erc8004 config set --identity-registry
\nbeam x erc8004 register [--uri |--empty-uri] [--identity-registry
]\nbeam x erc8004 show [--fetch-uri] [--identity-registry
]\nbeam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--identity-registry
]\nbeam x erc8004 set-uri [--identity-registry
]\nbeam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]\nbeam x erc8004 unset-wallet [--identity-registry
]\n```\n\nDefault ERC-8004 identity registry addresses are manifest-scoped. Custom\nregistry addresses come from app-local config or an explicit\n`--identity-registry` flag and are included as invocation-scoped contract rules\nin host calls and action plans.\n\n`list` uses `eth_getLogs` through the Beam host. The host enforces a bounded\nblock range and the app defaults to the active wallet with owner filtering, so\nit does not scan from genesis unless the user passes a broad explicit range.\n\n`set-wallet` resolves the wallet argument through Beam and requests an EIP-712\ntyped-data signature from the host. The app receives only the signature and\ndigest, never raw private keys.\n", + "manifest_summary": { + "format_version": 1, + "min_beam_version": "0.2.4", + "wasm_entrypoint": "beam_app_main" + }, + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4" + } + ], + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" + } +} diff --git a/beam-apps/fixtures/unsupported-beam/catalog/apps/erc8004.json.sig b/beam-apps/fixtures/unsupported-beam/catalog/apps/erc8004.json.sig new file mode 100644 index 0000000..0c9c285 --- /dev/null +++ b/beam-apps/fixtures/unsupported-beam/catalog/apps/erc8004.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" +} diff --git a/beam-apps/fixtures/unsupported-beam/index.json b/beam-apps/fixtures/unsupported-beam/index.json index b3923c1..6ab4c79 100644 --- a/beam-apps/fixtures/unsupported-beam/index.json +++ b/beam-apps/fixtures/unsupported-beam/index.json @@ -2,6 +2,27 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4", + "manifest_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/manifest.json", + "manifest_sha256": "sha256:ddbfd3a3e4c03eb65003a2f44c231d85d22e4a2b9de9173b33894202cb2c3c3a", + "module_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/module.wasm", + "module_sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" + } + } + ] + }, { "id": "uniswap", "name": "Uniswap", @@ -12,13 +33,13 @@ "version": "1.0.2", "min_beam_version": "0.2.1", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/manifest.json", - "manifest_sha256": "sha256:b3bee997c062dbf20de57c4176a010b3578bf4a5e7c8a57017733bd5a95e2b73", + "manifest_sha256": "sha256:36c9fc35b9d855622201146244eeb165e97592cbd333296d7e3f1454f37be545", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/module.wasm", - "module_sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "module_sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } } ] @@ -27,6 +48,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } } diff --git a/beam-apps/fixtures/unsupported-beam/index.json.sig b/beam-apps/fixtures/unsupported-beam/index.json.sig index 4193503..30b012f 100644 --- a/beam-apps/fixtures/unsupported-beam/index.json.sig +++ b/beam-apps/fixtures/unsupported-beam/index.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } diff --git a/beam-apps/fixtures/valid/apps/erc8004/1.0.0/icon.svg b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/icon.svg new file mode 100644 index 0000000..91a16e6 --- /dev/null +++ b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/beam-apps/fixtures/valid/apps/erc8004/1.0.0/manifest.json b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/manifest.json new file mode 100644 index 0000000..f69e711 --- /dev/null +++ b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/manifest.json @@ -0,0 +1,480 @@ +{ + "format_version": 1, + "id": "erc8004", + "display_name": "ERC-8004", + "version": "1.0.0", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "min_beam_version": "0.2.4", + "wasm": { + "sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "entrypoint": "beam_app_main" + }, + "catalog": { + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + } + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + } + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + } + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + } + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + } + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + } + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "sensitive_args": [], + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + } + } + ], + "permissions": { + "http": [ + { + "url": "https://*" + } + ], + "chains": [ + { + "chain": "*", + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ], + "contracts": [ + "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "0x8004A818BFB912233c491871b3d84c89A494BD9e" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ] + } + ], + "wallet": { + "read_balances": false, + "propose_transactions": true, + "erc20_approval": false, + "sign_typed_data": true + }, + "storage": { + "app_local": true + }, + "privacy": [] + }, + "host_api": { + "privacy_reserved": true + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" + } +} diff --git a/beam-apps/fixtures/valid/apps/erc8004/1.0.0/manifest.json.sig b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/manifest.json.sig new file mode 100644 index 0000000..33f51cd --- /dev/null +++ b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/manifest.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:6ee2a1305f9f220a7270739de96db804fb63bc62d3bab2b8987e2731a978ea61" +} diff --git a/beam-apps/fixtures/valid/apps/erc8004/1.0.0/module.wasm b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/module.wasm new file mode 100644 index 0000000..6e620ea Binary files /dev/null and b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/valid/apps/erc8004/1.0.0/version.json.sig b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/version.json.sig new file mode 100644 index 0000000..ecd262c --- /dev/null +++ b/beam-apps/fixtures/valid/apps/erc8004/1.0.0/version.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" +} diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json index 4b4bf56..1113f05 100644 --- a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json +++ b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.2.1", "wasm": { - "sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "entrypoint": "beam_app_main" }, "catalog": { @@ -290,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } } diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json.sig b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json.sig index 53eeada..53f61c1 100644 --- a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json.sig +++ b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a529b79e6b23e784176b3f2f78334d26b5d7b8dba15b66262829eda3b15d360b" + "value": "sha256:6d2507ea791ccd9965f992f55e08bad878ceb9929611edb8fa32195994ce3588" } diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/module.wasm b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/module.wasm index 4d88a6a..1d43407 100644 Binary files a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/module.wasm and b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/module.wasm differ diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/version.json.sig b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/version.json.sig index 250c2e2..d1cb93b 100644 --- a/beam-apps/fixtures/valid/apps/uniswap/1.0.2/version.json.sig +++ b/beam-apps/fixtures/valid/apps/uniswap/1.0.2/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } diff --git a/beam-apps/fixtures/valid/catalog/apps.json b/beam-apps/fixtures/valid/catalog/apps.json index dbba548..b03a654 100644 --- a/beam-apps/fixtures/valid/catalog/apps.json +++ b/beam-apps/fixtures/valid/catalog/apps.json @@ -2,6 +2,48 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "install_command": "beam apps install erc8004", + "pinned_install_command": "beam apps install erc8004 --version 1.0.0", + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "version": { + "version": "1.0.0", + "min_beam_version": "0.2.4" + }, + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, { "id": "uniswap", "display_name": "Uniswap", @@ -104,6 +146,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } } diff --git a/beam-apps/fixtures/valid/catalog/apps.json.sig b/beam-apps/fixtures/valid/catalog/apps.json.sig index 4d6ed71..d6d261d 100644 --- a/beam-apps/fixtures/valid/catalog/apps.json.sig +++ b/beam-apps/fixtures/valid/catalog/apps.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:066b1321d86de07e799ba77ac8a531d7a14c258b5ff79cb6e7f352a6cf8ab5d7" + "value": "sha256:f2ea0c84b76eea041e7289cc81adfcaaaffdb0e996b4c475c1622deb457c9c28" } diff --git a/beam-apps/fixtures/valid/catalog/apps/erc8004.json b/beam-apps/fixtures/valid/catalog/apps/erc8004.json new file mode 100644 index 0000000..7356f71 --- /dev/null +++ b/beam-apps/fixtures/valid/catalog/apps/erc8004.json @@ -0,0 +1,485 @@ +{ + "format_version": 1, + "generated_at": "2026-05-26T00:00:00Z", + "detail_url": "https://registry.beam.payy.network/catalog/apps/erc8004.json", + "app": { + "id": "erc8004", + "display_name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "latest_version": "1.0.0", + "min_beam_version": "0.2.4", + "install_commands": { + "latest": "beam apps install erc8004", + "pinned": "beam apps install erc8004 --version 1.0.0", + "dry_run": "beam apps install erc8004 --dry-run" + }, + "supported_chains": [ + { + "id": "*", + "label": "Any EVM chain", + "testnet": false, + "operations": [ + "read", + "logs", + "send-transaction", + "sign-typed-data" + ] + } + ], + "capability_badges": [ + "ERC-8004", + "Chain read", + "Logs", + "Onchain TX", + "Typed-data signing", + "App storage" + ], + "permission_summary": { + "http": [ + "https://*" + ], + "wallet": [ + "propose transactions" + ], + "selectors": [ + "0x1aa3a008", + "0xf2c298be", + "0x0af28bd3", + "0x2d1ef5ae", + "0x3fddcf19", + "0x6352211e", + "0xc87b56dd", + "0x00339509" + ], + "storage": [ + "app-local" + ], + "privacy": [] + }, + "commands": [ + { + "name": "support", + "about": "Show the active chain ERC-8004 registry configuration", + "usage": "support", + "docs": { + "summary": "Show ERC-8004 support for the active chain.", + "invocation": "beam x erc8004 support", + "arguments": [], + "options": [], + "examples": [ + { + "title": "Show support", + "command": "beam x erc8004 support --chain base", + "description": "Print the Base ERC-8004 registry addresses." + } + ], + "output_notes": [ + "Includes the identity registry and whether it is default or overridden." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "config", + "about": "Show or set ERC-8004 registry overrides", + "usage": "config show | config set --identity-registry
[--reputation-registry
]", + "docs": { + "summary": "Show or persist registry overrides for the active chain.", + "invocation": "beam x erc8004 config show | config set --identity-registry
[--reputation-registry
]", + "arguments": [], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Identity registry override for the active chain." + }, + { + "name": "--reputation-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Optional reputation registry override stored for future versions." + } + ], + "examples": [ + { + "title": "Set override", + "command": "beam x erc8004 config set --identity-registry 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432", + "description": "Persist an identity registry address for the active chain." + } + ], + "output_notes": [ + "Registry overrides are stored in app-local Beam storage." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "register", + "about": "Prepare an ERC-8004 agent registration", + "usage": "register [--uri |--empty-uri] [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that registers an ERC-8004 agent.", + "invocation": "beam x erc8004 register [--uri |--empty-uri] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--uri", + "value_name": "uri", + "kind": "string", + "required": false, + "sensitive": false, + "description": "HTTPS, IPFS, or data URI for the agent metadata." + }, + { + "name": "--empty-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Register without an agent URI." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Register", + "command": "beam x erc8004 register --uri https://agent.example/agent.json", + "description": "Prepare and approve a registration transaction." + } + ], + "output_notes": [ + "Returns an action plan that Beam approves and executes." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "show", + "about": "Read an ERC-8004 agent", + "usage": "show [--fetch-uri] [--identity-registry
]", + "docs": { + "summary": "Read owner, URI, and agent wallet for an ERC-8004 agent.", + "invocation": "beam x erc8004 show [--fetch-uri] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--fetch-uri", + "kind": "flag", + "required": false, + "default": "false", + "sensitive": false, + "description": "Fetch HTTPS agent metadata through Beam's safe HTTP host." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Show agent", + "command": "beam x erc8004 show 1 --fetch-uri", + "description": "Read an agent and fetch HTTPS metadata if available." + } + ], + "output_notes": [ + "Non-HTTPS URIs are not fetched." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "list", + "about": "List ERC-8004 agents connected to a wallet", + "usage": "list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "docs": { + "summary": "List ERC-8004 registrations using bounded log reads.", + "invocation": "beam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--from-block ] [--to-block ] [--identity-registry
]", + "arguments": [], + "options": [ + { + "name": "--wallet", + "value_name": "wallet", + "kind": "string", + "required": false, + "sensitive": false, + "description": "Beam wallet name, ENS name, or EVM address; defaults to the active wallet." + }, + { + "name": "--connection", + "value_name": "mode", + "kind": "enum", + "required": false, + "default": "owner", + "sensitive": false, + "description": "Filter by owner, agent wallet, or both." + }, + { + "name": "--from-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "Start block. Defaults to a bounded recent host window." + }, + { + "name": "--to-block", + "value_name": "number", + "kind": "integer", + "required": false, + "sensitive": false, + "description": "End block. Defaults to latest." + } + ], + "examples": [ + { + "title": "List owned agents", + "command": "beam x erc8004 list --wallet alice --from-block 1000000", + "description": "List agents registered by alice in a bounded block range." + } + ], + "output_notes": [ + "The host caps log ranges and response size." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-uri", + "about": "Prepare an ERC-8004 agent URI update", + "usage": "set-uri [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that updates an agent URI.", + "invocation": "beam x erc8004 set-uri [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "uri", + "kind": "string", + "required": true, + "sensitive": false, + "description": "New HTTPS, IPFS, or data URI." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Update URI", + "command": "beam x erc8004 set-uri 1 https://agent.example/new.json", + "description": "Prepare and approve an agent URI update." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "set-wallet", + "about": "Prepare an ERC-8004 agent wallet update", + "usage": "set-wallet [--deadline-seconds ] [--identity-registry
]", + "docs": { + "summary": "Request a typed-data signature from the target wallet and prepare the wallet update transaction.", + "invocation": "beam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + }, + { + "name": "wallet", + "kind": "string", + "required": true, + "sensitive": false, + "description": "Beam wallet name or stored EVM address selector that signs the update." + } + ], + "options": [ + { + "name": "--deadline-seconds", + "value_name": "seconds", + "kind": "integer", + "required": false, + "default": "300", + "sensitive": false, + "description": "Signature validity window, capped at 300 seconds." + }, + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Set named wallet", + "command": "beam x erc8004 set-wallet 1 alice", + "description": "Resolve alice from Beam wallets, request its typed-data signature, then prepare the registry update." + } + ], + "output_notes": [ + "The app never receives raw private keys." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + }, + { + "name": "unset-wallet", + "about": "Prepare clearing an ERC-8004 agent wallet", + "usage": "unset-wallet [--identity-registry
]", + "docs": { + "summary": "Prepare a transaction that clears an agent wallet.", + "invocation": "beam x erc8004 unset-wallet [--identity-registry
]", + "arguments": [ + { + "name": "agent-id", + "kind": "integer", + "required": true, + "sensitive": false, + "description": "ERC-8004 token ID." + } + ], + "options": [ + { + "name": "--identity-registry", + "value_name": "address", + "kind": "address", + "required": false, + "sensitive": false, + "description": "Invocation-scoped identity registry override." + } + ], + "examples": [ + { + "title": "Unset wallet", + "command": "beam x erc8004 unset-wallet 1", + "description": "Prepare and approve clearing the agent wallet." + } + ], + "output_notes": [ + "Returns an action plan." + ] + }, + "input_schema": { + "type": "object" + }, + "output_schema": { + "type": "object" + }, + "sensitive_args": [] + } + ], + "readme_markdown": "# ERC-8004 Beam App\n\nThe ERC-8004 app manages identity-registry agents through Beam's generic app\nhost. It keeps registry defaults and overrides in app space rather than as a\nnative Beam command.\n\n```text\nbeam x erc8004 support\nbeam x erc8004 config show\nbeam x erc8004 config set --identity-registry
\nbeam x erc8004 register [--uri |--empty-uri] [--identity-registry
]\nbeam x erc8004 show [--fetch-uri] [--identity-registry
]\nbeam x erc8004 list [--wallet ] [--connection owner|agent-wallet|both] [--identity-registry
]\nbeam x erc8004 set-uri [--identity-registry
]\nbeam x erc8004 set-wallet [--deadline-seconds ] [--identity-registry
]\nbeam x erc8004 unset-wallet [--identity-registry
]\n```\n\nDefault ERC-8004 identity registry addresses are manifest-scoped. Custom\nregistry addresses come from app-local config or an explicit\n`--identity-registry` flag and are included as invocation-scoped contract rules\nin host calls and action plans.\n\n`list` uses `eth_getLogs` through the Beam host. The host enforces a bounded\nblock range and the app defaults to the active wallet with owner filtering, so\nit does not scan from genesis unless the user passes a broad explicit range.\n\n`set-wallet` resolves the wallet argument through Beam and requests an EIP-712\ntyped-data signature from the host. The app receives only the signature and\ndigest, never raw private keys.\n", + "manifest_summary": { + "format_version": 1, + "min_beam_version": "0.2.4", + "wasm_entrypoint": "beam_app_main" + }, + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4" + } + ], + "icon": { + "url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/icon.svg", + "sha256": "sha256:16d48252ec062fb0461d9d78f99c104d0f187b6c81de666beb342e88289839a9", + "media_type": "image/svg+xml", + "alt": "ERC-8004 app icon" + } + }, + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" + } +} diff --git a/beam-apps/fixtures/valid/catalog/apps/erc8004.json.sig b/beam-apps/fixtures/valid/catalog/apps/erc8004.json.sig new file mode 100644 index 0000000..0c9c285 --- /dev/null +++ b/beam-apps/fixtures/valid/catalog/apps/erc8004.json.sig @@ -0,0 +1,5 @@ +{ + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:a2c8bc569cbdd56910bb378795b54f23fe5ab460a8e6cd52f3df37e7ccf45537" +} diff --git a/beam-apps/fixtures/valid/index.json b/beam-apps/fixtures/valid/index.json index b3923c1..6ab4c79 100644 --- a/beam-apps/fixtures/valid/index.json +++ b/beam-apps/fixtures/valid/index.json @@ -2,6 +2,27 @@ "format_version": 1, "generated_at": "2026-05-26T00:00:00Z", "apps": [ + { + "id": "erc8004", + "name": "ERC-8004", + "publisher": "Payy", + "description": "Manage ERC-8004 identity registry agents through Beam app permissions, bounded logs, typed-data signing, and action plans.", + "versions": [ + { + "version": "1.0.0", + "min_beam_version": "0.2.4", + "manifest_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/manifest.json", + "manifest_sha256": "sha256:ddbfd3a3e4c03eb65003a2f44c231d85d22e4a2b9de9173b33894202cb2c3c3a", + "module_url": "https://registry.beam.payy.network/apps/erc8004/1.0.0/module.wasm", + "module_sha256": "sha256:5a67ac5972280c99dd5738eec7bdc57a6beecf90902c43a1bbbeabfb145b7036", + "signature": { + "algorithm": "sha256-dev", + "key_id": "payy-dev-2026-05", + "value": "sha256:baca7450d2f0fac5e7110d6bff85f030295b6acd94f6db994d3f16fca91dc062" + } + } + ] + }, { "id": "uniswap", "name": "Uniswap", @@ -12,13 +33,13 @@ "version": "1.0.2", "min_beam_version": "0.2.1", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/manifest.json", - "manifest_sha256": "sha256:b3bee997c062dbf20de57c4176a010b3578bf4a5e7c8a57017733bd5a95e2b73", + "manifest_sha256": "sha256:36c9fc35b9d855622201146244eeb165e97592cbd333296d7e3f1454f37be545", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.2/module.wasm", - "module_sha256": "sha256:58c5dbc8343f5281392269b72e0193b2c39f6a4b36942df1bea464699a36cdc2", + "module_sha256": "sha256:a5ca0f4124e59390c0c40c64e2792ae04712f5fdb9accf92fdfa76be8a8702d7", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:447860c5bbc66e4f9b418bb2c67155a5c276812828da8c417a20387882fee0dc" + "value": "sha256:29febe94eebd4d2f004cb67449b6c03b7c6c8c41767ecec78e7997adb25536eb" } } ] @@ -27,6 +48,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } } diff --git a/beam-apps/fixtures/valid/index.json.sig b/beam-apps/fixtures/valid/index.json.sig index 4193503..30b012f 100644 --- a/beam-apps/fixtures/valid/index.json.sig +++ b/beam-apps/fixtures/valid/index.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:f1ad11d143f310620be1bc78a9d02fb7db7ec51d05acae195c1115167aef3dee" + "value": "sha256:94a96a25f08f9a5c4ed5dedbac299886f42e24af5ee43e49cf8c5adb90f1c695" } diff --git a/pkg/beam-cli/Cargo.toml b/pkg/beam-cli/Cargo.toml index 0302795..0ed61da 100644 --- a/pkg/beam-cli/Cargo.toml +++ b/pkg/beam-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "beam-cli" -version = "0.2.3" +version = "0.2.4" edition = "2024" publish = false diff --git a/pkg/beam-cli/README.md b/pkg/beam-cli/README.md index f70afa2..cdcbbfc 100644 --- a/pkg/beam-cli/README.md +++ b/pkg/beam-cli/README.md @@ -229,6 +229,7 @@ digests before caching app artifacts under `~/.beam/apps`. Common commands: ```bash +beam apps install erc8004 beam apps install uniswap beam apps list beam apps info uniswap @@ -253,14 +254,28 @@ beam x uniswap --help beam x uniswap swap --help beam --chain base --from alice x uniswap swap USDC ETH 100 --prepare beam apps run uniswap swap USDC ETH 100 --chain base --from alice --prepare +beam --chain base --from alice x erc8004 support +beam --chain base --from alice x erc8004 register --uri https://agent.example/agent.json +beam --chain base --from alice x erc8004 set-wallet 1 alice ``` Product app business logic lives outside Beam CLI in `beam-apps/apps/`. Beam CLI owns the generic registry, cache, WASM validation, permission checks, -host ABI, approval records, and execution of approved action plans. The Uniswap -app is built into the registry as WASM and `beam x uniswap swap ...` runs through -the generic guest command path; Beam CLI no longer contains a Uniswap-specific -built-in planner. +host ABI, approval records, and execution of approved action plans. Product apps +such as Uniswap and ERC-8004 are built into the registry as WASM and run through +the generic guest command path; Beam CLI does not contain product-specific +built-in planners. + +ERC-8004 agent identity management is provided by the `erc8004` app rather than +a native `beam agents` command. Default identity registry addresses are declared +in the app manifest. Custom registry addresses can be persisted with: + +```bash +beam x erc8004 config set --identity-registry
+``` + +Per-command registry overrides use `--identity-registry
` and are +validated as invocation-scoped contract permissions in the app host. The Uniswap app will use Beam-mediated HTTPS requests to the Uniswap Trading API. Release registry builds inject the Payy-managed public Trading API key into @@ -288,6 +303,20 @@ beam apps approvals show beam apps approvals approve --execute ``` +Beam prices EVM app transactions at approval and execution time. Apps may +propose transaction calldata, value, target, and gas-limit hints, but app +`gas_price`, `maxFeePerGas`, or similar fee fields are informational only and +are not used as the final signed transaction price. On EIP-1559 chains Beam +prefers type-2 transactions; legacy `gas_price` is a fallback for chains that do +not expose EIP-1559 fee history. + +Approval prompts and approval JSON include the maximum approved network fee per +transaction step. Pass `--max-network-fee-wei ` to `beam x ...` or +`beam apps run ...` to set a hard per-step network-fee cap; if omitted, +Beam stores a default cap based on the prepared estimate. Execution re-estimates +fees before signing and fails closed if current network fees exceed the approved +cap. + Uniswap token arguments can be configured token labels, `native`, native chain symbols, or EVM token addresses. Swap options include `--min-receive`, `--slippage-bps`, `--deadline-seconds`, `--recipient`, `--max-gas`, and diff --git a/pkg/beam-cli/src/apps/app_storage.rs b/pkg/beam-cli/src/apps/app_storage.rs new file mode 100644 index 0000000..5b76ace --- /dev/null +++ b/pkg/beam-cli/src/apps/app_storage.rs @@ -0,0 +1,62 @@ +use std::{fs, path::PathBuf}; + +use contextful::ResultContextExt; +use serde_json::{Value, json}; + +use crate::{ + apps::{Error, Result}, + runtime::BeamApp, +}; + +pub fn get(app: &BeamApp, app_id: &str, key: &str) -> Result { + let path = path(app, app_id, key)?; + if !path.exists() { + return Ok(json!({ "value": null, "exists": false })); + } + let value = + serde_json::from_slice::(&fs::read(path).context("read beam app storage value")?) + .context("decode beam app storage value")?; + Ok(json!({ "value": value, "exists": true })) +} + +pub fn set(app: &BeamApp, app_id: &str, key: &str, value: Value) -> Result { + let path = path(app, app_id, key)?; + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).context("create beam app storage directory")?; + } + fs::write( + path, + serde_json::to_vec_pretty(&value).context("encode beam app storage value")?, + ) + .context("write beam app storage value")?; + Ok(json!(true)) +} + +pub fn remove(app: &BeamApp, app_id: &str, key: &str) -> Result { + let path = path(app, app_id, key)?; + if path.exists() { + fs::remove_file(path).context("remove beam app storage value")?; + } + Ok(json!(true)) +} + +fn path(app: &BeamApp, app_id: &str, key: &str) -> Result { + if key.is_empty() + || key.starts_with('.') + || !key + .chars() + .all(|char| char.is_ascii_alphanumeric() || matches!(char, '-' | '_' | '.')) + { + return Err(Error::InvalidHostRequest { + reason: format!("invalid app storage key {key}"), + }); + } + + Ok(app + .paths + .root + .join("apps") + .join("data") + .join(app_id) + .join(key)) +} diff --git a/pkg/beam-cli/src/apps/approvals.rs b/pkg/beam-cli/src/apps/approvals.rs index d453722..57d0764 100644 --- a/pkg/beam-cli/src/apps/approvals.rs +++ b/pkg/beam-cli/src/apps/approvals.rs @@ -7,7 +7,7 @@ use sha2::{Digest, Sha256}; use crate::apps::{ Error, Result, - model::{ActionPlan, ApprovalRecord, ApprovalStatus, ApprovalsState}, + model::{ActionPlan, ApprovalFeeCap, ApprovalRecord, ApprovalStatus, ApprovalsState}, store::now, }; @@ -32,7 +32,11 @@ impl ApprovalStore { self.store.get().await.approvals } - pub async fn create(&self, plan: ActionPlan) -> Result { + pub async fn create( + &self, + plan: ActionPlan, + fee_caps: Vec, + ) -> Result { let created_at = now(); let plan_hash = plan_hash(&plan)?; let id = format!("apr_{}", &plan_hash["sha256:".len()..18]); @@ -41,6 +45,7 @@ impl ApprovalStore { status: ApprovalStatus::Pending, plan, plan_hash, + fee_caps, created_at, updated_at: created_at, }; diff --git a/pkg/beam-cli/src/apps/chain_logs.rs b/pkg/beam-cli/src/apps/chain_logs.rs new file mode 100644 index 0000000..c346f1c --- /dev/null +++ b/pkg/beam-cli/src/apps/chain_logs.rs @@ -0,0 +1,129 @@ +use contextful::ResultContextExt; +use contracts::Client; +use serde_json::{Value, json}; +use web3::types::{BlockNumber, FilterBuilder, H256, Log}; + +use crate::apps::{ + Error, Result, + host::{ChainReadRequest, parse_host_address}, +}; + +const MAX_LOG_BLOCK_RANGE: u64 = 50_000; +const MAX_LOG_TOPICS: usize = 4; +const MAX_LOG_TOPIC_VALUES: usize = 16; +const MAX_LOG_RESPONSE_BYTES: usize = 1024 * 1024; + +pub async fn read(client: &Client, request: &ChainReadRequest) -> Result { + let target = parse_host_address( + "target", + request + .target + .as_deref() + .ok_or_else(|| Error::InvalidHostRequest { + reason: "chain read missing target".to_string(), + })?, + )?; + let latest = client + .block_number() + .await + .context("fetch beam app latest block")? + .as_u64(); + let to_block = request.to_block.unwrap_or(latest).min(latest); + let from_block = request + .from_block + .unwrap_or_else(|| to_block.saturating_sub(MAX_LOG_BLOCK_RANGE)); + if from_block > to_block { + return Err(Error::InvalidHostRequest { + reason: format!("invalid log block range {from_block}..{to_block}"), + }); + } + if to_block.saturating_sub(from_block) > MAX_LOG_BLOCK_RANGE { + return Err(Error::InvalidHostRequest { + reason: format!("log block range exceeds {MAX_LOG_BLOCK_RANGE} blocks"), + }); + } + + let topics = parse_topics(&request.topics)?; + let filter = FilterBuilder::default() + .address(vec![target]) + .from_block(BlockNumber::Number(from_block.into())) + .to_block(BlockNumber::Number(to_block.into())) + .topics( + topics.first().cloned().unwrap_or(None), + topics.get(1).cloned().unwrap_or(None), + topics.get(2).cloned().unwrap_or(None), + topics.get(3).cloned().unwrap_or(None), + ) + .build(); + let logs = client.logs(filter).await.context("fetch beam app logs")?; + let value = json!({ + "from_block": from_block, + "logs": logs_json(&logs), + "target": format!("{target:#x}"), + "to_block": to_block, + }); + let bytes = serde_json::to_vec(&value).context("measure beam app log response")?; + if bytes.len() > MAX_LOG_RESPONSE_BYTES { + return Err(Error::InvalidHostRequest { + reason: format!("log response exceeds {MAX_LOG_RESPONSE_BYTES} bytes"), + }); + } + + Ok(value) +} + +fn parse_topics(topics: &[Option>]) -> Result>>> { + if topics.len() > MAX_LOG_TOPICS { + return Err(Error::InvalidHostRequest { + reason: format!("log query supports at most {MAX_LOG_TOPICS} topic positions"), + }); + } + let mut out = Vec::new(); + for topic in topics { + let Some(values) = topic else { + out.push(None); + continue; + }; + if values.len() > MAX_LOG_TOPIC_VALUES { + return Err(Error::InvalidHostRequest { + reason: format!( + "log query supports at most {MAX_LOG_TOPIC_VALUES} values per topic" + ), + }); + } + let mut parsed = Vec::new(); + for value in values { + parsed.push( + value + .parse::() + .map_err(|_| Error::InvalidHostRequest { + reason: format!("invalid log topic {value}"), + })?, + ); + } + out.push(Some(parsed)); + } + + Ok(out) +} + +fn logs_json(logs: &[Log]) -> Vec { + logs.iter() + .map(|log| { + json!({ + "address": format!("{:#x}", log.address), + "block_hash": log.block_hash.map(|value| format!("{value:#x}")), + "block_number": log.block_number.map(|value| value.as_u64()), + "data": format!("0x{}", hex::encode(&log.data.0)), + "log_index": log.log_index.map(|value| value.to_string()), + "topics": log + .topics + .iter() + .map(|topic| format!("{topic:#x}")) + .collect::>(), + "transaction_hash": log.transaction_hash.map(|value| format!("{value:#x}")), + "transaction_index": log.transaction_index.map(|value| value.to_string()), + }) + }) + .collect() +} diff --git a/pkg/beam-cli/src/apps/error.rs b/pkg/beam-cli/src/apps/error.rs index 1d4186e..366f088 100644 --- a/pkg/beam-cli/src/apps/error.rs +++ b/pkg/beam-cli/src/apps/error.rs @@ -81,6 +81,12 @@ pub enum Error { #[error("[beam-cli/apps] app requested blocked approval spender: {spender}")] SpenderPermissionDenied { spender: String }, + #[error("[beam-cli/apps] app requested blocked wallet permission: {permission}")] + WalletPermissionDenied { permission: String }, + + #[error("[beam-cli/apps] app requested blocked storage permission: {permission}")] + StoragePermissionDenied { permission: String }, + #[error("[beam-cli/apps] app requested blocked http url: {url}")] HttpPermissionDenied { url: String }, @@ -120,6 +126,14 @@ pub enum Error { #[error("[beam-cli/apps] approval plan changed: {approval_id}")] ApprovalPlanChanged { approval_id: String }, + #[error( + "[beam-cli/apps] approval fee cap missing for executable step {step_index}; reapprove the app action" + )] + ApprovalFeeCapMissing { step_index: usize }, + + #[error("[beam-cli/apps] approval needs fresh fee caps before execution: {approval_id}")] + ApprovalNeedsFeeCaps { approval_id: String }, + #[error( "[beam-cli/apps] approval execution context changed for {approval_id}: {field} expected {expected}, got {actual}" )] diff --git a/pkg/beam-cli/src/apps/host.rs b/pkg/beam-cli/src/apps/host.rs index f342d8e..173a393 100644 --- a/pkg/beam-cli/src/apps/host.rs +++ b/pkg/beam-cli/src/apps/host.rs @@ -1,5 +1,5 @@ -// lint-long-file-override allow-max-lines=700 -use std::{fs, net::IpAddr, path::PathBuf, time::Duration}; +// lint-long-file-override allow-max-lines=800 +use std::{net::IpAddr, time::Duration}; use contextful::ResultContextExt; use contracts::{Address, U256}; @@ -14,9 +14,13 @@ use web3::types::{Bytes, CallRequest}; use crate::{ apps::{ - Error, Result, - model::{AppPermissions, ChainOperation}, - permissions::{ensure_chain_scope, glob_matches}, + Error, Result, app_storage, chain_logs, + model::{AppPermissions, ChainOperation, DynamicContractScope}, + permissions::{ + ensure_chain_scope_with_dynamic, glob_matches, normalize_dynamic_contracts, + validate_dynamic_contracts, + }, + typed_data, }, evm::{erc20_allowance, erc20_balance, native_balance, simulate_calldata}, runtime::{BeamApp, ResolvedToken}, @@ -51,6 +55,7 @@ pub enum HostRequest { Diagnostic { level: String, message: String }, HttpFetch(HttpFetchRequest), ChainRead(ChainReadRequest), + SignTypedData(TypedDataSignRequest), SimulateTransaction(HostTransaction), SubmitTransaction(HostTransaction), PollReceipt { tx_hash: String }, @@ -119,6 +124,10 @@ pub struct ChainReadRequest { #[serde(default)] pub data: Option, #[serde(default)] + pub dynamic_contracts: Vec, + #[serde(default)] + pub from_block: Option, + #[serde(default)] pub owner: Option, #[serde(default)] pub spender: Option, @@ -126,6 +135,10 @@ pub struct ChainReadRequest { #[serde(default)] pub token: Option, #[serde(default)] + pub topics: Vec>>, + #[serde(default)] + pub to_block: Option, + #[serde(default)] pub value: Option, pub selector: Option, } @@ -138,6 +151,7 @@ pub enum ChainReadOperation { Balance, Allowance, Call, + Logs, Nonce, Gas, } @@ -146,12 +160,35 @@ pub enum ChainReadOperation { pub struct HostTransaction { pub chain: String, pub data: String, + #[serde(default)] + pub dynamic_contracts: Vec, pub target: String, pub value: String, pub selector: Option, pub spender: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct TypedDataSignRequest { + pub chain: String, + #[serde(default)] + pub dynamic_contracts: Vec, + pub domain_separator: String, + #[serde(default)] + pub fields: Vec, + pub primary_type: String, + pub struct_hash: String, + pub verifying_contract: String, + pub wallet: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct TypedDataDisplayField { + pub name: String, + pub kind: String, + pub value: String, +} + pub async fn handle_host_request( app: &BeamApp, permissions: &AppPermissions, @@ -176,6 +213,7 @@ pub async fn handle_host_request( } HostRequest::HttpFetch(request) => Ok(json!(fetch_http(permissions, request).await?)), HostRequest::ChainRead(request) => chain_read(app, permissions, request).await, + HostRequest::SignTypedData(request) => typed_data::sign(app, permissions, request).await, HostRequest::SimulateTransaction(transaction) => { let (_, client) = app .active_chain_client() @@ -209,57 +247,28 @@ pub async fn handle_host_request( Ok(json!({ "address": format!("{address:#x}") })) } HostRequest::AppStorageGet { key } => { - let path = app_storage_path(app, &metadata.app_id, &key)?; - if !path.exists() { - return Ok(json!({ "value": null, "exists": false })); - } - let value = serde_json::from_slice::( - &fs::read(path).context("read beam app storage value")?, - ) - .context("decode beam app storage value")?; - Ok(json!({ "value": value, "exists": true })) + ensure_app_storage_allowed(permissions)?; + app_storage::get(app, &metadata.app_id, &key) } HostRequest::AppStorageSet { key, value } => { - let path = app_storage_path(app, &metadata.app_id, &key)?; - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).context("create beam app storage directory")?; - } - fs::write( - path, - serde_json::to_vec_pretty(&value).context("encode beam app storage value")?, - ) - .context("write beam app storage value")?; - Ok(json!({ "written": true })) + ensure_app_storage_allowed(permissions)?; + app_storage::set(app, &metadata.app_id, &key, value) } HostRequest::AppStorageRemove { key } => { - let path = app_storage_path(app, &metadata.app_id, &key)?; - if path.exists() { - fs::remove_file(path).context("remove beam app storage value")?; - } - Ok(json!({ "removed": true })) + ensure_app_storage_allowed(permissions)?; + app_storage::remove(app, &metadata.app_id, &key) } } } -fn app_storage_path(app: &BeamApp, app_id: &str, key: &str) -> Result { - if key.is_empty() - || key.starts_with('.') - || !key - .chars() - .all(|char| char.is_ascii_alphanumeric() || matches!(char, '-' | '_' | '.')) - { - return Err(Error::InvalidHostRequest { - reason: format!("invalid app storage key {key}"), - }); +pub fn ensure_app_storage_allowed(permissions: &AppPermissions) -> Result<()> { + if permissions.storage.app_local { + return Ok(()); } - Ok(app - .paths - .root - .join("apps") - .join("data") - .join(app_id) - .join(key)) + Err(Error::StoragePermissionDenied { + permission: "app-local".to_string(), + }) } pub fn ensure_http_allowed(permissions: &AppPermissions, url: &str) -> Result { @@ -349,10 +358,16 @@ pub fn ensure_chain_read_allowed( permissions: &AppPermissions, request: &ChainReadRequest, ) -> Result<()> { - ensure_chain_scope( + let operation = match request.operation { + ChainReadOperation::Logs => ChainOperation::Logs, + _ => ChainOperation::Read, + }; + validate_dynamic_contracts(&request.dynamic_contracts, &request.chain)?; + ensure_chain_scope_with_dynamic( permissions, + &normalize_dynamic_contracts(&request.dynamic_contracts), &request.chain, - ChainOperation::Read, + operation, request.target.as_deref(), request.selector.as_deref(), None, @@ -463,6 +478,13 @@ pub async fn chain_read( .context("execute beam app chain read call")?; Ok(json!({ "raw": format!("0x{}", hex::encode(raw.0)) })) } + ChainReadOperation::Logs => { + let (_, client) = app + .active_chain_client() + .await + .context("connect beam app chain client")?; + chain_logs::read(&client, &request).await + } ChainReadOperation::Nonce => { let (_, client) = app .active_chain_client() @@ -524,8 +546,10 @@ pub fn ensure_transaction_allowed( transaction: &HostTransaction, operation: ChainOperation, ) -> Result<()> { - ensure_chain_scope( + validate_dynamic_contracts(&transaction.dynamic_contracts, &transaction.chain)?; + ensure_chain_scope_with_dynamic( permissions, + &normalize_dynamic_contracts(&transaction.dynamic_contracts), &transaction.chain, operation, Some(&transaction.target), @@ -674,13 +698,13 @@ fn is_native_token(token: &str, native_symbol: &str) -> bool { || token.eq_ignore_ascii_case("0x0000000000000000000000000000000000000000") } -fn parse_hex_data(value: &str) -> Result> { +pub(super) fn parse_hex_data(value: &str) -> Result> { hex::decode(value.strip_prefix("0x").unwrap_or(value)).map_err(|_| Error::InvalidHostRequest { reason: format!("invalid hex data {value}"), }) } -fn parse_host_address(field: &str, value: &str) -> Result
{ +pub(super) fn parse_host_address(field: &str, value: &str) -> Result
{ value.parse().map_err(|_| Error::InvalidHostRequest { reason: format!("invalid {field} address {value}"), }) diff --git a/pkg/beam-cli/src/apps/mod.rs b/pkg/beam-cli/src/apps/mod.rs index ccc0a7d..a66b4f0 100644 --- a/pkg/beam-cli/src/apps/mod.rs +++ b/pkg/beam-cli/src/apps/mod.rs @@ -1,4 +1,6 @@ +pub(crate) mod app_storage; pub mod approvals; +mod chain_logs; mod error; pub mod host; pub mod model; @@ -7,6 +9,7 @@ pub mod privacy; pub mod registry; pub mod runtime; pub mod store; +mod typed_data; pub mod validate; pub(crate) use error::format_error_chain; diff --git a/pkg/beam-cli/src/apps/model.rs b/pkg/beam-cli/src/apps/model.rs index e30e373..0eb019a 100644 --- a/pkg/beam-cli/src/apps/model.rs +++ b/pkg/beam-cli/src/apps/model.rs @@ -1,4 +1,4 @@ -// lint-long-file-override allow-max-lines=300 +// lint-long-file-override allow-max-lines=400 use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -167,9 +167,11 @@ pub struct ChainPermission { #[serde(rename_all = "kebab-case")] pub enum ChainOperation { Read, + Logs, Simulate, SendTransaction, Erc20Approval, + SignTypedData, } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] @@ -180,6 +182,8 @@ pub struct WalletPermissions { pub propose_transactions: bool, #[serde(default)] pub erc20_approval: bool, + #[serde(default, skip_serializing_if = "is_false")] + pub sign_typed_data: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] @@ -229,6 +233,10 @@ pub struct AppLock { pub installed_at: u64, } +fn is_false(value: &bool) -> bool { + !*value +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ActionPlan { pub app_id: String, @@ -244,9 +252,19 @@ pub struct ActionPlan { pub bindings: Vec, #[serde(default)] pub constraints: Vec, + #[serde(default)] + pub dynamic_contracts: Vec, pub expires_at: u64, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct DynamicContractScope { + pub chain: String, + pub contract: String, + #[serde(default)] + pub reason: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ActionBinding { pub key: String, @@ -271,10 +289,23 @@ pub struct ApprovalRecord { pub status: ApprovalStatus, pub plan: ActionPlan, pub plan_hash: String, + #[serde(default)] + pub fee_caps: Vec, pub created_at: u64, pub updated_at: u64, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct ApprovalFeeCap { + pub step_index: usize, + pub approved_gas_limit: String, + pub approved_max_fee_per_gas: String, + pub approved_max_total_fee_wei: String, + pub fee_mode: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub approved_max_priority_fee_per_gas: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum ApprovalStatus { diff --git a/pkg/beam-cli/src/apps/permissions.rs b/pkg/beam-cli/src/apps/permissions.rs index bff9ab0..cedac7c 100644 --- a/pkg/beam-cli/src/apps/permissions.rs +++ b/pkg/beam-cli/src/apps/permissions.rs @@ -1,10 +1,11 @@ use crate::apps::{ Error, Result, - model::{AppPermissions, ChainOperation, ChainPermission}, + model::{AppPermissions, ChainOperation, ChainPermission, DynamicContractScope}, }; -pub fn ensure_chain_scope( +pub fn ensure_chain_scope_with_dynamic( permissions: &AppPermissions, + dynamic_contracts: &[DynamicContractScope], chain: &str, operation: ChainOperation, target: Option<&str>, @@ -13,10 +14,14 @@ pub fn ensure_chain_scope( ) -> Result<()> { let scope = chain_scope(permissions, chain, &operation)?; if let Some(target) = target { - ensure_optional_scope(scope.contracts.as_deref(), target).map_err(|_| { - Error::ContractPermissionDenied { - target: target.to_string(), - } + ensure_optional_contract_scope( + scope.contracts.as_deref(), + dynamic_contracts, + chain, + target, + ) + .map_err(|_| Error::ContractPermissionDenied { + target: target.to_string(), })?; } if let Some(selector) = selector { @@ -37,6 +42,49 @@ pub fn ensure_chain_scope( Ok(()) } +pub fn normalize_dynamic_contracts( + dynamic_contracts: &[DynamicContractScope], +) -> Vec { + let mut out = Vec::new(); + for scope in dynamic_contracts { + let normalized_contract = scope.contract.to_ascii_lowercase(); + if out.iter().any(|existing: &DynamicContractScope| { + glob_matches(&existing.chain, &scope.chain) + && existing.contract.eq_ignore_ascii_case(&normalized_contract) + }) { + continue; + } + out.push(DynamicContractScope { + chain: scope.chain.clone(), + contract: normalized_contract, + reason: scope.reason.clone(), + }); + } + + out +} + +pub fn validate_dynamic_contracts( + dynamic_contracts: &[DynamicContractScope], + chain: &str, +) -> Result<()> { + for scope in dynamic_contracts { + if !glob_matches(&scope.chain, chain) { + return Err(Error::ChainPermissionDenied { + chain: scope.chain.clone(), + operation: "dynamic-contract".to_string(), + }); + } + if scope.contract.parse::().is_err() { + return Err(Error::InvalidHostRequest { + reason: format!("invalid dynamic contract {}", scope.contract), + }); + } + } + + Ok(()) +} + pub fn glob_matches(pattern: &str, value: &str) -> bool { let pattern = pattern.to_ascii_lowercase(); let value = value.to_ascii_lowercase(); @@ -77,3 +125,21 @@ fn ensure_optional_scope(patterns: Option<&[String]>, value: &str) -> std::resul None => Ok(()), } } + +fn ensure_optional_contract_scope( + patterns: Option<&[String]>, + dynamic_contracts: &[DynamicContractScope], + chain: &str, + target: &str, +) -> std::result::Result<(), ()> { + if ensure_optional_scope(patterns, target).is_ok() { + return Ok(()); + } + if dynamic_contracts.iter().any(|scope| { + glob_matches(&scope.chain, chain) && scope.contract.eq_ignore_ascii_case(target) + }) { + return Ok(()); + } + + Err(()) +} diff --git a/pkg/beam-cli/src/apps/runtime/debug.rs b/pkg/beam-cli/src/apps/runtime/debug.rs index 1595a49..12d5892 100644 --- a/pkg/beam-cli/src/apps/runtime/debug.rs +++ b/pkg/beam-cli/src/apps/runtime/debug.rs @@ -42,6 +42,10 @@ pub(super) fn host_request_summary(request: &HostRequest) -> String { optional_value(&request.target), optional_value(&request.selector) ), + HostRequest::SignTypedData(request) => format!( + "sign-typed-data chain={} verifying_contract={} primary_type={}", + request.chain, request.verifying_contract, request.primary_type + ), HostRequest::SimulateTransaction(transaction) => format!( "simulate-transaction chain={} target={} selector={} spender={}", transaction.chain, diff --git a/pkg/beam-cli/src/apps/typed_data.rs b/pkg/beam-cli/src/apps/typed_data.rs new file mode 100644 index 0000000..c429fa5 --- /dev/null +++ b/pkg/beam-cli/src/apps/typed_data.rs @@ -0,0 +1,135 @@ +use std::io::Write; + +use contextful::ResultContextExt; +use contracts::Address; +use secp256k1::{Message, SECP256K1, SecretKey}; +use serde_json::{Value, json}; +use web3::signing::keccak256; + +use crate::{ + apps::{ + Error, Result, + host::{TypedDataSignRequest, parse_hex_data, parse_host_address}, + model::{AppPermissions, ChainOperation}, + permissions::{ + ensure_chain_scope_with_dynamic, normalize_dynamic_contracts, + validate_dynamic_contracts, + }, + }, + keystore::{decrypt_private_key, prompt_existing_password}, + runtime::BeamApp, +}; + +pub async fn sign( + app: &BeamApp, + permissions: &AppPermissions, + request: TypedDataSignRequest, +) -> Result { + if !permissions.wallet.sign_typed_data { + return Err(Error::WalletPermissionDenied { + permission: "sign-typed-data".to_string(), + }); + } + validate_dynamic_contracts(&request.dynamic_contracts, &request.chain)?; + let (chain, _) = app + .active_chain_client() + .await + .context("connect beam app signing chain client")?; + if chain.entry.key != request.chain { + return Err(Error::ChainPermissionDenied { + chain: request.chain, + operation: "sign-typed-data".to_string(), + }); + } + ensure_chain_scope_with_dynamic( + permissions, + &normalize_dynamic_contracts(&request.dynamic_contracts), + &chain.entry.key, + ChainOperation::SignTypedData, + Some(&request.verifying_contract), + None, + None, + )?; + + let wallet = app + .resolve_wallet(&request.wallet) + .await + .context("resolve beam app typed-data signing wallet")?; + let signer_address = + wallet + .address + .parse::
() + .map_err(|_| Error::InvalidHostRequest { + reason: format!("invalid signing wallet address {}", wallet.address), + })?; + let verifying_contract = parse_host_address("verifying contract", &request.verifying_contract)?; + let domain_separator = parse_hash32("domain_separator", &request.domain_separator)?; + let struct_hash = parse_hash32("struct_hash", &request.struct_hash)?; + let digest = typed_data_digest(domain_separator, struct_hash); + + prompt_signature(&request, &chain.entry.key, signer_address, digest)?; + let password = prompt_existing_password().context("read typed-data signing password")?; + let private_key = + decrypt_private_key(&wallet, &password).context("decrypt typed-data signing wallet")?; + let secret_key = + SecretKey::from_slice(&private_key).map_err(|_| Error::InvalidHostRequest { + reason: "invalid signing private key".to_string(), + })?; + let message = Message::from_digest(digest); + let signature = SECP256K1.sign_ecdsa_recoverable(&message, &secret_key); + let (recovery_id, compact) = signature.serialize_compact(); + let recovery_id = + u8::try_from(recovery_id.to_i32()).map_err(|_| Error::InvalidHostRequest { + reason: "invalid signature recovery id".to_string(), + })?; + let mut out = Vec::with_capacity(65); + out.extend_from_slice(&compact); + out.push(27u8.saturating_add(recovery_id)); + + Ok(json!({ + "chain": chain.entry.key, + "digest": format!("0x{}", hex::encode(digest)), + "primary_type": request.primary_type, + "signature": format!("0x{}", hex::encode(out)), + "signer": format!("{signer_address:#x}"), + "verifying_contract": format!("{verifying_contract:#x}"), + })) +} + +fn prompt_signature( + request: &TypedDataSignRequest, + chain: &str, + signer: Address, + digest: [u8; 32], +) -> Result<()> { + let mut stderr = std::io::stderr().lock(); + writeln!(stderr, "Beam app typed-data signature request").context("write signature prompt")?; + writeln!(stderr, "App chain: {chain}").context("write signature prompt")?; + writeln!(stderr, "Signing wallet: {signer:#x}").context("write signature prompt")?; + writeln!(stderr, "Verifying contract: {}", request.verifying_contract) + .context("write signature prompt")?; + writeln!(stderr, "Primary type: {}", request.primary_type).context("write signature prompt")?; + writeln!(stderr, "Typed-data digest: 0x{}", hex::encode(digest)) + .context("write signature prompt")?; + for field in &request.fields { + writeln!(stderr, "{} {} = {}", field.kind, field.name, field.value) + .context("write signature prompt")?; + } + + Ok(()) +} + +fn parse_hash32(field: &str, value: &str) -> Result<[u8; 32]> { + let bytes = parse_hex_data(value)?; + bytes.try_into().map_err(|_| Error::InvalidHostRequest { + reason: format!("{field} must be 32 bytes"), + }) +} + +fn typed_data_digest(domain_separator: [u8; 32], struct_hash: [u8; 32]) -> [u8; 32] { + let mut digest_input = Vec::with_capacity(66); + digest_input.extend_from_slice(&[0x19, 0x01]); + digest_input.extend_from_slice(&domain_separator); + digest_input.extend_from_slice(&struct_hash); + keccak256(&digest_input) +} diff --git a/pkg/beam-cli/src/cli.rs b/pkg/beam-cli/src/cli.rs index f9da6f5..3136cc0 100644 --- a/pkg/beam-cli/src/cli.rs +++ b/pkg/beam-cli/src/cli.rs @@ -1,4 +1,4 @@ -// lint-long-file-override allow-max-lines=330 +// lint-long-file-override allow-max-lines=300 mod apps; mod chain; mod contract; diff --git a/pkg/beam-cli/src/cli/apps.rs b/pkg/beam-cli/src/cli/apps.rs index 2f659da..53049b6 100644 --- a/pkg/beam-cli/src/cli/apps.rs +++ b/pkg/beam-cli/src/cli/apps.rs @@ -48,6 +48,8 @@ pub struct AppRunArgs { pub prepare: bool, #[arg(long, default_value_t = false)] pub no_prompt: bool, + #[arg(long)] + pub max_network_fee_wei: Option, #[arg(trailing_var_arg = true, allow_hyphen_values = true)] pub args: Vec, } @@ -63,6 +65,8 @@ pub enum AppApprovalAction { approval_id: String, #[arg(long, default_value_t = false)] execute: bool, + #[arg(long)] + max_network_fee_wei: Option, }, /// Reject an app approval continuation Reject { approval_id: String }, diff --git a/pkg/beam-cli/src/commands/apps/args.rs b/pkg/beam-cli/src/commands/apps/args.rs new file mode 100644 index 0000000..78523b3 --- /dev/null +++ b/pkg/beam-cli/src/commands/apps/args.rs @@ -0,0 +1,32 @@ +use crate::cli::APP_HELP_ARG; + +pub(super) fn filtered_app_args(args: &[String]) -> Vec { + let mut filtered = Vec::new(); + let mut index = 0; + while index < args.len() { + let arg = &args[index]; + if arg == "--prepare" || arg == "--no-prompt" { + index += 1; + continue; + } + if arg == "--max-network-fee-wei" { + index += 2; + continue; + } + if arg.starts_with("--max-network-fee-wei=") { + index += 1; + continue; + } + filtered.push(if arg == APP_HELP_ARG { + "--help".to_string() + } else { + arg.clone() + }); + index += 1; + } + filtered +} + +pub(super) fn is_help_requested(args: &[String]) -> bool { + args.iter().any(|arg| arg == "--help" || arg == "-h") +} diff --git a/pkg/beam-cli/src/commands/apps/execution.rs b/pkg/beam-cli/src/commands/apps/execution.rs index 6a07530..bb30297 100644 --- a/pkg/beam-cli/src/commands/apps/execution.rs +++ b/pkg/beam-cli/src/commands/apps/execution.rs @@ -5,11 +5,14 @@ use serde_json::{Value, json}; use crate::{ apps::{ Error as AppError, - model::{ActionPlan, ActionStep}, + model::{ActionPlan, ActionStep, ApprovalFeeCap}, }, commands::signing::prompt_active_signer, error::{Error, Result}, - evm::{CalldataTransaction, TransactionGas, erc20_allowance, send_calldata_with_gas}, + evm::{ + CalldataTransaction, TransactionGasPolicy, erc20_allowance, send_calldata_with_fee_report, + transaction_fee_json, + }, output::{ CommandOutput, confirmed_transaction_message, dropped_transaction_message, pending_transaction_message, with_loading_handle, @@ -19,16 +22,29 @@ use crate::{ transaction::{TransactionExecution, loading_message}, }; -pub async fn execute_plan(app: &BeamApp, plan: &ActionPlan) -> Result { +pub async fn execute_plan( + app: &BeamApp, + plan: &ActionPlan, + fee_caps: &[ApprovalFeeCap], +) -> Result { + let signer = prompt_active_signer(app).await?; + execute_plan_with_signer(app, plan, fee_caps, &signer).await +} + +pub(crate) async fn execute_plan_with_signer( + app: &BeamApp, + plan: &ActionPlan, + fee_caps: &[ApprovalFeeCap], + signer: &S, +) -> Result { let executable = plan.steps.iter().any(|step| transaction(step).is_some()); if !executable { return Ok(render_simulated_execution(plan)); } let (chain, client) = app.active_chain_client().await?; - let signer = prompt_active_signer(app).await?; let mut outputs = Vec::new(); - for step in &plan.steps { + for (step_index, step) in plan.steps.iter().enumerate() { let Some(transaction) = transaction(step) else { continue; }; @@ -38,6 +54,7 @@ pub async fn execute_plan(app: &BeamApp, plan: &ActionPlan) -> Result Result Result String { lines.join("\n") } -fn step_output(step: &ActionStep, execution: TransactionExecution) -> Value { +fn step_output(step: &ActionStep, execution: TransactionExecution, fee: Value) -> Value { match execution { TransactionExecution::Confirmed(outcome) => json!({ "block_number": outcome.block_number, + "fee": fee, "state": "confirmed", "status": outcome.status, "summary": confirmed_transaction_message(&step.summary, &outcome.tx_hash, outcome.block_number), @@ -131,6 +153,7 @@ fn step_output(step: &ActionStep, execution: TransactionExecution) -> Value { }), TransactionExecution::Pending(pending) => json!({ "block_number": pending.block_number, + "fee": fee, "state": "pending", "status": null, "summary": pending_transaction_message(&step.summary, &pending.tx_hash, pending.block_number), @@ -138,6 +161,7 @@ fn step_output(step: &ActionStep, execution: TransactionExecution) -> Value { }), TransactionExecution::Dropped(dropped) => json!({ "block_number": dropped.block_number, + "fee": fee, "state": "dropped", "status": null, "summary": dropped_transaction_message(&step.summary, &dropped.tx_hash, dropped.block_number), @@ -203,10 +227,6 @@ impl TransactionValue<'_> { self.optional_string("gas_limit") } - fn gas_price(&self) -> Option<&str> { - self.optional_string("gas_price") - } - fn to(&self) -> Result<&str> { self.string("to") } @@ -228,14 +248,19 @@ impl TransactionValue<'_> { } } -fn parse_gas(transaction: &TransactionValue<'_>) -> Result> { - match (transaction.gas_limit(), transaction.gas_price()) { - (Some(gas_limit), Some(gas_price)) => Ok(Some(TransactionGas { - gas_limit: parse_u256(gas_limit)?, - gas_price: parse_u256(gas_price)?, - })), - _ => Ok(None), - } +fn parse_gas_policy( + step_index: usize, + transaction: &TransactionValue<'_>, + fee_caps: &[ApprovalFeeCap], +) -> Result> { + let fee_cap = fee_caps + .iter() + .find(|fee_cap| fee_cap.step_index == step_index) + .ok_or(AppError::ApprovalFeeCapMissing { step_index })?; + Ok(Some(TransactionGasPolicy { + gas_limit: transaction.gas_limit().map(parse_u256).transpose()?, + max_network_fee: Some(parse_u256(&fee_cap.approved_max_total_fee_wei)?), + })) } fn parse_hex_data(value: &str) -> Result> { @@ -248,7 +273,5 @@ fn parse_u256(value: &str) -> Result { if let Some(value) = value.strip_prefix("0x") { return Ok(contracts::U256::from_str_radix(value, 16).context("parse hex u256")?); } - Ok(value - .parse::() - .context("parse decimal u256")?) + Ok(contracts::U256::from_dec_str(value).context("parse decimal u256")?) } diff --git a/pkg/beam-cli/src/commands/apps/fee_caps.rs b/pkg/beam-cli/src/commands/apps/fee_caps.rs new file mode 100644 index 0000000..ac56355 --- /dev/null +++ b/pkg/beam-cli/src/commands/apps/fee_caps.rs @@ -0,0 +1,218 @@ +// lint-long-file-override allow-max-lines=300 +use contracts::U256; +use serde_json::Value; + +use super::{prompt::approve_interactively, render}; +use crate::{ + apps::{ + Error as AppError, + model::{ActionPlan, ActionStep, ApprovalFeeCap, ApprovalRecord}, + }, + error::{Error, Result}, + evm::{EvmFeeEstimate, TransactionGas, TransactionGasPolicy, resolve_transaction_gas}, + output::OutputMode, + runtime::{BeamApp, parse_address}, +}; + +const DEFAULT_APPROVAL_FEE_CAP_MULTIPLIER: u64 = 2; + +pub(super) async fn approval_fee_caps( + app: &BeamApp, + plan: &ActionPlan, + user_max_network_fee: Option, +) -> Result> { + let executable_steps = plan + .steps + .iter() + .any(|step| transaction_metadata(step).is_some()); + if !executable_steps { + return Ok(Vec::new()); + } + + let (_, client) = app.active_chain_client().await?; + let from = app.active_address().await?; + let mut caps = Vec::new(); + for (step_index, step) in plan.steps.iter().enumerate() { + let Some(transaction) = transaction_metadata(step) else { + continue; + }; + let to = parse_address(transaction.string("to")?)?; + let data = parse_hex_data(transaction.string("data")?)?; + let value = transaction + .optional_string("value") + .map(parse_u256) + .transpose()? + .unwrap_or_else(U256::zero); + let gas_limit = transaction + .optional_string("gas_limit") + .map(parse_u256) + .transpose()?; + let gas = resolve_transaction_gas( + &client, + from, + to, + &data, + value, + Some(TransactionGasPolicy { + gas_limit, + max_network_fee: user_max_network_fee, + }), + ) + .await?; + let approved_max_total_fee = user_max_network_fee.unwrap_or_else(|| { + gas.max_network_fee() * U256::from(DEFAULT_APPROVAL_FEE_CAP_MULTIPLIER) + }); + caps.push(approval_fee_cap(step_index, gas, approved_max_total_fee)); + } + + Ok(caps) +} + +fn approval_fee_cap( + step_index: usize, + gas: TransactionGas, + approved_max_total_fee: U256, +) -> ApprovalFeeCap { + let approved_max_fee_per_gas = if gas.gas_limit.is_zero() { + U256::zero() + } else { + approved_max_total_fee / gas.gas_limit + }; + let (fee_mode, approved_max_priority_fee_per_gas) = match gas.fee { + EvmFeeEstimate::Legacy { .. } => ("legacy", None), + EvmFeeEstimate::Eip1559 { + max_priority_fee_per_gas, + .. + } => ("eip1559", Some(max_priority_fee_per_gas.to_string())), + }; + + ApprovalFeeCap { + step_index, + approved_gas_limit: gas.gas_limit.to_string(), + approved_max_fee_per_gas: approved_max_fee_per_gas.to_string(), + approved_max_total_fee_wei: approved_max_total_fee.to_string(), + fee_mode: fee_mode.to_string(), + approved_max_priority_fee_per_gas, + } +} + +pub(super) fn parse_max_network_fee(value: &str) -> Result { + parse_u256(value) +} + +pub(super) fn max_network_fee_arg( + cli_value: Option<&str>, + args: &[String], +) -> Result> { + let trailing_value = trailing_max_network_fee_arg(args)?; + match (cli_value, trailing_value.as_deref()) { + (Some(value), Some(trailing)) if value != trailing => Err(AppError::InvalidHostRequest { + reason: "conflicting --max-network-fee-wei values".to_string(), + } + .into()), + (Some(value), _) | (_, Some(value)) => Ok(Some(parse_max_network_fee(value)?)), + (None, None) => Ok(None), + } +} + +pub(super) async fn approval_fee_caps_for_execution( + app: &BeamApp, + approval: &ApprovalRecord, + max_network_fee_wei: Option<&str>, +) -> Result> { + if !approval.fee_caps.is_empty() { + return Ok(approval.fee_caps.clone()); + } + if app.output_mode != OutputMode::Default { + return Err(AppError::ApprovalNeedsFeeCaps { + approval_id: approval.id.clone(), + } + .into()); + } + + let max_network_fee = max_network_fee_wei.map(parse_max_network_fee).transpose()?; + let fee_caps = approval_fee_caps(app, &approval.plan, max_network_fee).await?; + approve_interactively(&render::render_plan_with_fee_caps( + &approval.plan, + &fee_caps, + ))?; + Ok(fee_caps) +} + +fn trailing_max_network_fee_arg(args: &[String]) -> Result> { + let mut value = None; + let mut index = 0; + while index < args.len() { + let arg = &args[index]; + if let Some(arg_value) = arg.strip_prefix("--max-network-fee-wei=") { + value = merge_max_network_fee_arg(value, arg_value)?; + index += 1; + continue; + } + if arg == "--max-network-fee-wei" { + let Some(arg_value) = args.get(index + 1) else { + return Err(AppError::InvalidHostRequest { + reason: "--max-network-fee-wei requires a value".to_string(), + } + .into()); + }; + value = merge_max_network_fee_arg(value, arg_value)?; + index += 2; + continue; + } + index += 1; + } + Ok(value) +} + +fn merge_max_network_fee_arg(existing: Option, next: &str) -> Result> { + if let Some(existing) = existing + && existing != next + { + return Err(AppError::InvalidHostRequest { + reason: "conflicting --max-network-fee-wei values".to_string(), + } + .into()); + } + Ok(Some(next.to_string())) +} + +fn transaction_metadata(step: &ActionStep) -> Option> { + step.metadata + .get("transaction") + .and_then(Value::as_object) + .map(TransactionMetadata) +} + +struct TransactionMetadata<'a>(&'a serde_json::Map); + +impl TransactionMetadata<'_> { + fn string(&self, key: &str) -> Result<&str> { + self.optional_string(key).ok_or_else(|| { + Error::App(AppError::InvalidHostRequest { + reason: format!("transaction missing {key}"), + }) + }) + } + + fn optional_string(&self, key: &str) -> Option<&str> { + self.0.get(key).and_then(Value::as_str) + } +} + +fn parse_hex_data(value: &str) -> Result> { + hex::decode(value.strip_prefix("0x").unwrap_or(value)).map_err(|_| Error::InvalidHexData { + value: value.to_string(), + }) +} + +fn parse_u256(value: &str) -> Result { + if let Some(value) = value.strip_prefix("0x") { + return U256::from_str_radix(value, 16).map_err(|_| Error::InvalidNumber { + value: value.to_string(), + }); + } + U256::from_dec_str(value).map_err(|_| Error::InvalidNumber { + value: value.to_string(), + }) +} diff --git a/pkg/beam-cli/src/commands/apps/mod.rs b/pkg/beam-cli/src/commands/apps/mod.rs index a801603..ca8baa3 100644 --- a/pkg/beam-cli/src/commands/apps/mod.rs +++ b/pkg/beam-cli/src/commands/apps/mod.rs @@ -1,8 +1,12 @@ // lint-long-file-override allow-max-lines=400 +mod args; mod execution; +mod fee_caps; mod plans; mod prompt; mod render; +#[cfg(test)] +mod tests; use std::fs; @@ -22,14 +26,16 @@ use crate::{ store::AppCache, validate::ensure_beam_version, }, - cli::{APP_HELP_ARG, AppApprovalAction, AppInstallArgs, AppRemoveArgs, AppRunArgs, AppsAction}, + cli::{AppApprovalAction, AppInstallArgs, AppRemoveArgs, AppRunArgs, AppsAction}, error::Result, output::CommandOutput, runtime::BeamApp, table::render_table, }; +use args::{filtered_app_args, is_help_requested}; use execution::execute_plan; +use fee_caps::{approval_fee_caps, approval_fee_caps_for_execution, max_network_fee_arg}; use plans::{validate_guest_plan, validate_plan_permissions}; use prompt::approve_interactively; use render::{ @@ -54,6 +60,7 @@ pub async fn run(app: &BeamApp, action: AppsAction) -> Result<()> { pub async fn run_app(app: &BeamApp, args: AppRunArgs) -> Result<()> { let prepare = args.prepare || args.args.iter().any(|arg| arg == "--prepare"); let no_prompt = args.no_prompt || args.args.iter().any(|arg| arg == "--no-prompt"); + let max_network_fee = max_network_fee_arg(args.max_network_fee_wei.as_deref(), &args.args)?; let command_args = filtered_app_args(&args.args); let cache = AppCache::load(&app.paths.root).await?; let (installed, manifest) = cache.active_manifest(&args.app).await?; @@ -106,10 +113,15 @@ pub async fn run_app(app: &BeamApp, args: AppRunArgs) -> Result<()> { validate_guest_plan(app, &manifest, &installed, &command_args, &plan).await?; validate_plan_permissions(&manifest.permissions, &plan)?; let approval_required = plan_requires_approval(&plan); + let fee_caps = if approval_required { + approval_fee_caps(app, &plan, max_network_fee).await? + } else { + Vec::new() + }; if prepare { let approvals = ApprovalStore::load(&app.paths.root).await?; - let approval = approvals.create(plan).await?; + let approval = approvals.create(plan, fee_caps).await?; return render_approval_created(&approval).print(app.output_mode); } @@ -117,9 +129,11 @@ pub async fn run_app(app: &BeamApp, args: AppRunArgs) -> Result<()> { if no_prompt { return Err(AppError::ApprovalRequired.into()); } - approve_interactively(&render::render_plan(&plan))?; + approve_interactively(&render::render_plan_with_fee_caps(&plan, &fee_caps))?; } - execute_plan(app, &plan).await?.print(app.output_mode) + execute_plan(app, &plan, &fee_caps) + .await? + .print(app.output_mode) } fn plan_requires_approval(plan: &ActionPlan) -> bool { @@ -128,23 +142,6 @@ fn plan_requires_approval(plan: &ActionPlan) -> bool { .any(|step| step.kind == "erc20-approval" || step.kind == "transaction") } -fn filtered_app_args(args: &[String]) -> Vec { - args.iter() - .filter(|arg| arg.as_str() != "--prepare" && arg.as_str() != "--no-prompt") - .map(|arg| { - if arg == APP_HELP_ARG { - "--help".to_string() - } else { - arg.clone() - } - }) - .collect() -} - -fn is_help_requested(args: &[String]) -> bool { - args.iter().any(|arg| arg == "--help" || arg == "-h") -} - async fn install(app: &BeamApp, args: AppInstallArgs) -> Result<()> { let registry_url = registry_url_from_env(); let index = fetch_index(®istry_url).await?; @@ -310,12 +307,16 @@ async fn approvals(app: &BeamApp, action: AppApprovalAction) -> Result<()> { AppApprovalAction::Approve { approval_id, execute, + max_network_fee_wei, } => { let approval = store.find(&approval_id).await?; if execute { ensure_approval_executable(&approval)?; ensure_approval_matches_active(app, &approval).await?; - let output = execute_plan(app, &approval.plan).await?; + let fee_caps = + approval_fee_caps_for_execution(app, &approval, max_network_fee_wei.as_deref()) + .await?; + let output = execute_plan(app, &approval.plan, &fee_caps).await?; store.mark_executed(&approval_id).await?; return output.print(app.output_mode); } diff --git a/pkg/beam-cli/src/commands/apps/plans.rs b/pkg/beam-cli/src/commands/apps/plans.rs index 7860340..9031b6f 100644 --- a/pkg/beam-cli/src/commands/apps/plans.rs +++ b/pkg/beam-cli/src/commands/apps/plans.rs @@ -4,7 +4,10 @@ use crate::{ model::{ ActionPlan, ActionStep, AppManifest, AppPermissions, ChainOperation, InstalledApp, }, - permissions::ensure_chain_scope, + permissions::{ + ensure_chain_scope_with_dynamic, normalize_dynamic_contracts, + validate_dynamic_contracts, + }, store::now, }, error::Result, @@ -78,10 +81,13 @@ pub(super) fn validate_plan_permissions( permissions: &AppPermissions, plan: &ActionPlan, ) -> Result<()> { + validate_dynamic_contracts(&plan.dynamic_contracts, &plan.chain)?; + let dynamic_contracts = normalize_dynamic_contracts(&plan.dynamic_contracts); for step in &plan.steps { if let Some(target) = step.target.as_deref() { - ensure_chain_scope( + ensure_chain_scope_with_dynamic( permissions, + &dynamic_contracts, &plan.chain, operation_for_step(step), Some(target), @@ -90,8 +96,9 @@ pub(super) fn validate_plan_permissions( )?; } if let Some(selector) = step.selector.as_deref() { - ensure_chain_scope( + ensure_chain_scope_with_dynamic( permissions, + &dynamic_contracts, &plan.chain, operation_for_step(step), None, @@ -100,8 +107,9 @@ pub(super) fn validate_plan_permissions( )?; } if let Some(spender) = step.spender.as_deref() { - ensure_chain_scope( + ensure_chain_scope_with_dynamic( permissions, + &dynamic_contracts, &plan.chain, operation_for_step(step), None, diff --git a/pkg/beam-cli/src/commands/apps/render.rs b/pkg/beam-cli/src/commands/apps/render.rs index 6efc63f..a878667 100644 --- a/pkg/beam-cli/src/commands/apps/render.rs +++ b/pkg/beam-cli/src/commands/apps/render.rs @@ -4,7 +4,7 @@ use serde_json::{Value, json}; use crate::{ apps::model::{ ActionPlan, AppCommand, AppCommandExample, AppCommandParameter, AppManifest, - AppPermissions, ApprovalRecord, + AppPermissions, ApprovalFeeCap, ApprovalRecord, }, output::CommandOutput, }; @@ -62,10 +62,11 @@ pub(super) fn render_permissions(permissions: &AppPermissions) -> String { } lines.push("Wallet actions:".to_string()); lines.push(format!( - " - balances: {}\n - transaction proposals: {}\n - erc20 approvals: {}", + " - balances: {}\n - transaction proposals: {}\n - erc20 approvals: {}\n - typed-data signing: {}", permissions.wallet.read_balances, permissions.wallet.propose_transactions, - permissions.wallet.erc20_approval + permissions.wallet.erc20_approval, + permissions.wallet.sign_typed_data )); lines.push(format!( "Storage:\n - app-local: {}", @@ -168,14 +169,33 @@ fn push_examples(lines: &mut Vec, examples: &[AppCommandExample]) { } } -pub(super) fn render_plan(plan: &ActionPlan) -> String { +pub(super) fn render_plan_with_fee_caps(plan: &ActionPlan, fee_caps: &[ApprovalFeeCap]) -> String { let mut lines = vec![ format!("App: {} {}", plan.app_id, plan.app_version), format!("Chain: {}", plan.chain), "Action:".to_string(), ]; - for step in &plan.steps { + for (step_index, step) in plan.steps.iter().enumerate() { lines.push(format!(" - {}", step.summary)); + if let Some(fee_cap) = fee_caps + .iter() + .find(|fee_cap| fee_cap.step_index == step_index) + { + lines.push(format!( + " Max network fee: {} wei", + fee_cap.approved_max_total_fee_wei + )); + lines.push(format!( + " Approved gas limit: {}", + fee_cap.approved_gas_limit + )); + } + } + if !plan.dynamic_contracts.is_empty() { + lines.push("Invocation-scoped contracts:".to_string()); + for scope in &plan.dynamic_contracts { + lines.push(format!(" - {} on {}", scope.contract, scope.chain)); + } } lines.push(format!("Expires at: {}", plan.expires_at)); lines.join("\n") @@ -187,7 +207,7 @@ pub(super) fn render_approval(record: &ApprovalRecord) -> String { record.id, record.status, record.plan_hash, - render_plan(&record.plan) + render_plan_with_fee_caps(&record.plan, &record.fee_caps) ) } diff --git a/pkg/beam-cli/src/commands/apps/tests.rs b/pkg/beam-cli/src/commands/apps/tests.rs new file mode 100644 index 0000000..14cda91 --- /dev/null +++ b/pkg/beam-cli/src/commands/apps/tests.rs @@ -0,0 +1,272 @@ +// lint-long-file-override allow-max-lines=300 +use std::sync::{Arc, Mutex}; + +use contracts::{Address, U256}; +use serde_json::{Value, json}; +use tokio::{ + io::AsyncWriteExt, + net::{TcpListener, TcpStream}, +}; +use web3::types::{H256, TransactionReceipt, U64}; + +use super::execution::execute_plan_with_signer; +use crate::{ + apps::model::{ActionPlan, ActionStep, ApprovalFeeCap}, + error::Error, + output::OutputMode, + runtime::InvocationOverrides, + signer::KeySigner, + tests::fixtures::{read_rpc_request, test_app_with_output}, +}; + +const BASE_CHAIN_ID: u64 = 8453; +const MAX_FEE_PER_GAS: u64 = 4_000_000_000; +const MAX_PRIORITY_FEE_PER_GAS: u64 = 2_000_000_000; +const PADDED_GAS_LIMIT: u64 = 25_200; + +#[tokio::test] +async fn app_transaction_with_low_gas_price_is_repriced_as_eip1559() { + let (rpc_url, state, server) = spawn_app_execution_rpc_server().await; + let (_temp_dir, app) = test_app_with_output( + OutputMode::Quiet, + InvocationOverrides { + chain: Some("base".to_string()), + rpc: Some(rpc_url), + ..InvocationOverrides::default() + }, + ) + .await; + let signer = KeySigner::from_slice(&[7u8; 32]).expect("create signer"); + let output = execute_plan_with_signer(&app, &action_plan(), &fee_caps(), &signer) + .await + .expect("execute app plan"); + server.abort(); + + let raw_transaction = state + .lock() + .expect("rpc state") + .raw_transaction + .clone() + .expect("raw transaction"); + let signed = decode_typed_transaction(&raw_transaction); + + assert_eq!(signed.transaction_type, 2); + assert_eq!(signed.max_priority_fee_per_gas, MAX_PRIORITY_FEE_PER_GAS); + assert_eq!(signed.max_fee_per_gas, MAX_FEE_PER_GAS); + assert_eq!(output.value["steps"][0]["fee"]["fee_mode"], "eip1559"); + assert_eq!( + output.value["steps"][0]["fee"]["max_fee_per_gas"], + MAX_FEE_PER_GAS.to_string() + ); + assert_eq!( + output.value["steps"][0]["fee"]["max_network_fee_wei"], + (PADDED_GAS_LIMIT * MAX_FEE_PER_GAS).to_string() + ); + assert_eq!(output.value["steps"][0]["fee"].get("gas_price"), None); +} + +#[tokio::test] +async fn app_transaction_without_fee_cap_fails_closed() { + let (rpc_url, _state, server) = spawn_app_execution_rpc_server().await; + let (_temp_dir, app) = test_app_with_output( + OutputMode::Quiet, + InvocationOverrides { + chain: Some("base".to_string()), + rpc: Some(rpc_url), + ..InvocationOverrides::default() + }, + ) + .await; + let signer = KeySigner::from_slice(&[7u8; 32]).expect("create signer"); + + let error = execute_plan_with_signer(&app, &action_plan(), &[], &signer) + .await + .expect_err("reject missing fee cap"); + server.abort(); + + assert!(matches!( + error, + Error::App(crate::apps::Error::ApprovalFeeCapMissing { step_index: 0 }) + )); +} + +#[tokio::test] +async fn app_transaction_over_fee_cap_fails_before_submission() { + let (rpc_url, state, server) = spawn_app_execution_rpc_server().await; + let (_temp_dir, app) = test_app_with_output( + OutputMode::Quiet, + InvocationOverrides { + chain: Some("base".to_string()), + rpc: Some(rpc_url), + ..InvocationOverrides::default() + }, + ) + .await; + let signer = KeySigner::from_slice(&[7u8; 32]).expect("create signer"); + let mut fee_caps = fee_caps(); + fee_caps[0].approved_max_total_fee_wei = "1".to_string(); + + let error = execute_plan_with_signer(&app, &action_plan(), &fee_caps, &signer) + .await + .expect_err("reject fee cap breach"); + server.abort(); + + assert!(matches!(error, Error::TransactionFeeCapExceeded { .. })); + assert!(state.lock().expect("rpc state").raw_transaction.is_none()); +} + +#[derive(Default)] +struct AppExecutionRpcState { + raw_transaction: Option, +} + +struct DecodedTypedTransaction { + transaction_type: u8, + max_priority_fee_per_gas: u64, + max_fee_per_gas: u64, +} + +async fn spawn_app_execution_rpc_server() -> ( + String, + Arc>, + tokio::task::JoinHandle<()>, +) { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind app execution rpc listener"); + let address = listener.local_addr().expect("listener address"); + let state = Arc::new(Mutex::new(AppExecutionRpcState::default())); + let server_state = Arc::clone(&state); + + let server = tokio::spawn(async move { + loop { + let (stream, _peer) = listener.accept().await.expect("accept rpc connection"); + handle_app_execution_rpc_connection(stream, Arc::clone(&server_state)).await; + } + }); + + (format!("http://{address}"), state, server) +} + +async fn handle_app_execution_rpc_connection( + mut stream: TcpStream, + state: Arc>, +) { + let request = read_rpc_request(&mut stream).await; + let method = request["method"].as_str().expect("rpc method"); + if method == "eth_sendRawTransaction" { + state.lock().expect("rpc state").raw_transaction = Some( + request["params"][0] + .as_str() + .expect("raw transaction") + .to_string(), + ); + } + + let body = rpc_response(&request); + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", + body.len(), + body + ); + stream + .write_all(response.as_bytes()) + .await + .expect("write rpc response"); +} + +fn rpc_response(request: &Value) -> String { + let result = match request["method"].as_str().expect("rpc method") { + "eth_chainId" => serde_json::to_value(U256::from(BASE_CHAIN_ID)).expect("chain id"), + "eth_feeHistory" => json!({ + "oldestBlock": "0x1", + "baseFeePerGas": ["0x3b9aca00", "0x3b9aca00"], + "gasUsedRatio": [0.5], + "reward": [["0x77359400"]], + }), + "eth_getTransactionCount" => serde_json::to_value(U256::zero()).expect("nonce"), + "eth_sendRawTransaction" => serde_json::to_value(H256::from_low_u64_be(7)).expect("hash"), + "eth_getTransactionReceipt" => serde_json::to_value(successful_receipt()).expect("receipt"), + other => panic!("unexpected rpc method {other}"), + }; + + json!({ + "jsonrpc": "2.0", + "id": request["id"].clone(), + "result": result, + }) + .to_string() +} + +fn successful_receipt() -> TransactionReceipt { + TransactionReceipt { + block_number: Some(U64::from(42)), + status: Some(U64::from(1)), + transaction_hash: H256::from_low_u64_be(7), + ..Default::default() + } +} + +fn action_plan() -> ActionPlan { + ActionPlan { + app_id: "uniswap".to_string(), + app_version: "1.0.0".to_string(), + wasm_sha256: "sha256:wasm".to_string(), + manifest_sha256: "sha256:manifest".to_string(), + command: "swap USDC ETH 1".to_string(), + wallet: None, + chain: "base".to_string(), + steps: vec![ActionStep { + kind: "transaction".to_string(), + summary: "Swap 1 USDC for ETH".to_string(), + target: Some(format!("{:#x}", Address::from_low_u64_be(0xfeed))), + selector: Some("0x3593564c".to_string()), + spender: None, + value: Some("0".to_string()), + metadata: json!({ + "transaction": { + "data": "0x3593564c", + "gas_limit": "21000", + "gas_price": "1", + "to": format!("{:#x}", Address::from_low_u64_be(0xfeed)), + "value": "0", + }, + }), + }], + bindings: Vec::new(), + constraints: Vec::new(), + dynamic_contracts: Vec::new(), + expires_at: 9_999_999_999, + } +} + +fn fee_caps() -> Vec { + vec![ApprovalFeeCap { + step_index: 0, + approved_gas_limit: PADDED_GAS_LIMIT.to_string(), + approved_max_fee_per_gas: MAX_FEE_PER_GAS.to_string(), + approved_max_total_fee_wei: "200000000000000".to_string(), + fee_mode: "eip1559".to_string(), + approved_max_priority_fee_per_gas: Some(MAX_PRIORITY_FEE_PER_GAS.to_string()), + }] +} + +fn decode_typed_transaction(raw_transaction: &str) -> DecodedTypedTransaction { + let bytes = hex::decode(raw_transaction.trim_start_matches("0x")).expect("decode transaction"); + assert!(!bytes.is_empty(), "raw transaction should not be empty"); + let rlp = rlp::Rlp::new(&bytes[1..]); + DecodedTypedTransaction { + transaction_type: bytes[0], + max_priority_fee_per_gas: rlp_u64_at(&rlp, 2), + max_fee_per_gas: rlp_u64_at(&rlp, 3), + } +} + +fn rlp_u64_at(rlp: &rlp::Rlp<'_>, index: usize) -> u64 { + let data = rlp + .at(index) + .expect("decode rlp item") + .data() + .expect("decode rlp integer"); + U256::from_big_endian(data).as_u64() +} diff --git a/pkg/beam-cli/src/commands/fetch/payment.rs b/pkg/beam-cli/src/commands/fetch/payment.rs index f3663bb..45b83d2 100644 --- a/pkg/beam-cli/src/commands/fetch/payment.rs +++ b/pkg/beam-cli/src/commands/fetch/payment.rs @@ -16,7 +16,7 @@ use crate::{ commands::signing::prompt_active_signer, error::{Error, Result}, evm::{ - FunctionCall, TransactionGas, format_units, parse_units, send_function_with_gas, + FunctionCall, TransactionGasPolicy, format_units, parse_units, send_function_with_gas, send_native_with_gas, }, human_output::sanitize_control_chars, @@ -226,10 +226,10 @@ impl PreparedPayment { Ok(()) } - fn transaction_gas(&self) -> TransactionGas { - TransactionGas { - gas_limit: self.gas.gas_limit, - gas_price: self.gas.gas_price, + fn transaction_gas(&self) -> TransactionGasPolicy { + TransactionGasPolicy { + gas_limit: Some(self.gas.gas_limit), + max_network_fee: Some(self.gas.fee), } } diff --git a/pkg/beam-cli/src/commands/gas.rs b/pkg/beam-cli/src/commands/gas.rs index b6d881b..5e8a957 100644 --- a/pkg/beam-cli/src/commands/gas.rs +++ b/pkg/beam-cli/src/commands/gas.rs @@ -1,4 +1,4 @@ -// lint-long-file-override allow-max-lines=300 +// lint-long-file-override allow-max-lines=400 use serde_json::{Value, json}; use web3::ethabi::StateMutability; @@ -8,8 +8,8 @@ use crate::{ commands::call::{parse_transaction_value, resolve_address_args}, error::Result, evm::{ - FunctionCall, TransactionGas, erc20_decimals, estimate_function_gas, estimate_native_gas, - format_units, parse_units, + EvmFeeEstimate, EvmFeeMode, FunctionCall, TransactionGas, erc20_decimals, + estimate_function_gas, estimate_native_gas, format_units, parse_units, }, human_output::sanitize_control_chars, output::{CommandOutput, with_loading}, @@ -254,10 +254,27 @@ fn render_gas_output(config: GasOutputConfig<'_>) -> CommandOutput { "chain": config.chain_key, "estimated_fee": fee_display, "estimated_fee_wei": fee.to_string(), + "fee_mode": fee_mode_label(&config.gas.fee), "gas_limit": config.gas.gas_limit.to_string(), - "gas_price": config.gas.gas_price.to_string(), + "max_fee_per_gas": config.gas.gas_price_for_display().to_string(), "native_symbol": config.native_symbol, }); + if let Some(output) = value.as_object_mut() { + match config.gas.fee { + EvmFeeEstimate::Legacy { gas_price } => { + output.insert("gas_price".to_string(), json!(gas_price.to_string())); + } + EvmFeeEstimate::Eip1559 { + max_priority_fee_per_gas, + .. + } => { + output.insert( + "max_priority_fee_per_gas".to_string(), + json!(max_priority_fee_per_gas.to_string()), + ); + } + } + } if let Some(output) = value.as_object_mut() && let Some(extra) = config.extra.as_object() @@ -267,24 +284,33 @@ fn render_gas_output(config: GasOutputConfig<'_>) -> CommandOutput { CommandOutput::new( format!( - "{}\nEstimated fee: {} {} ({} wei)\nGas limit: {}\nGas price: {} wei", + "{}\nEstimated fee: {} {} ({} wei)\nGas limit: {}\nFee mode: {}\nMax fee per gas: {} wei", config.default_summary, fee_display, config.native_symbol, fee, config.gas.gas_limit, - config.gas.gas_price, + fee_mode_label(&config.gas.fee), + config.gas.gas_price_for_display(), ), value, ) .compact(fee_display.clone()) .markdown(format!( - "- Chain: `{}`\n- Estimated fee: `{}` `{}` (`{}` wei)\n- Gas limit: `{}`\n- Gas price: `{}` wei", + "- Chain: `{}`\n- Estimated fee: `{}` `{}` (`{}` wei)\n- Gas limit: `{}`\n- Fee mode: `{}`\n- Max fee per gas: `{}` wei", config.chain_key, fee_display, config.native_symbol, fee, config.gas.gas_limit, - config.gas.gas_price, + fee_mode_label(&config.gas.fee), + config.gas.gas_price_for_display(), )) } + +fn fee_mode_label(fee: &EvmFeeEstimate) -> &'static str { + match fee.mode() { + EvmFeeMode::Legacy => "legacy", + EvmFeeMode::Eip1559 => "eip1559", + } +} diff --git a/pkg/beam-cli/src/commands/gas/tests.rs b/pkg/beam-cli/src/commands/gas/tests.rs index a8d4f97..6494369 100644 --- a/pkg/beam-cli/src/commands/gas/tests.rs +++ b/pkg/beam-cli/src/commands/gas/tests.rs @@ -2,7 +2,7 @@ use contracts::U256; use serde_json::json; use super::{GasOutputConfig, render_gas_output}; -use crate::evm::TransactionGas; +use crate::evm::{EvmFeeEstimate, TransactionGas}; #[test] fn render_gas_output_includes_fee_details() { @@ -12,7 +12,9 @@ fn render_gas_output_includes_fee_details() { extra: json!({ "kind": "transfer" }), gas: TransactionGas { gas_limit: U256::from(21_000u64), - gas_price: U256::from(1_000_000_000u64), + fee: EvmFeeEstimate::Legacy { + gas_price: U256::from(1_000_000_000u64), + }, }, native_symbol: "ETH", }); @@ -20,7 +22,9 @@ fn render_gas_output_includes_fee_details() { assert!(output.default.contains("Estimated fee: 0.000021 ETH")); assert_eq!(output.value["estimated_fee"], "0.000021"); assert_eq!(output.value["estimated_fee_wei"], "21000000000000"); + assert_eq!(output.value["fee_mode"], "legacy"); assert_eq!(output.value["gas_limit"], "21000"); assert_eq!(output.value["gas_price"], "1000000000"); + assert_eq!(output.value["max_fee_per_gas"], "1000000000"); assert_eq!(output.value["kind"], "transfer"); } diff --git a/pkg/beam-cli/src/error.rs b/pkg/beam-cli/src/error.rs index 2ebd740..b1ac036 100644 --- a/pkg/beam-cli/src/error.rs +++ b/pkg/beam-cli/src/error.rs @@ -1,5 +1,6 @@ -// lint-long-file-override allow-max-lines=400 +// lint-long-file-override allow-max-lines=500 use contextful::{FromContextful, InternalError}; +use contracts::U256; use crate::apps::Error as AppError; @@ -323,6 +324,11 @@ pub enum Error { #[error("[beam-cli] transaction not found: {tx_hash}")] TransactionNotFound { tx_hash: String }, + #[error( + "[beam-cli] transaction network fee exceeds approved cap: estimated {estimated} wei, cap {cap} wei" + )] + TransactionFeeCapExceeded { cap: U256, estimated: U256 }, + #[error("[beam-cli] block not found: {block}")] BlockNotFound { block: String }, diff --git a/pkg/beam-cli/src/evm.rs b/pkg/beam-cli/src/evm.rs index b3469cd..c05efae 100644 --- a/pkg/beam-cli/src/evm.rs +++ b/pkg/beam-cli/src/evm.rs @@ -1,14 +1,14 @@ // lint-long-file-override allow-max-lines=400 +mod fees; mod gas; use contextful::ResultContextExt; use contracts::{Address, Client, ERC20Contract, U256}; use web3::{ ethabi::{Function, StateMutability}, - types::{Bytes, CallRequest, TransactionParameters, TransactionReceipt}, + types::{Bytes, CallRequest, TransactionParameters, TransactionReceipt, U64}, }; -use self::gas::resolve_transaction_gas; pub use crate::units::{format_units, parse_units, validate_unit_decimals}; use crate::{ abi::{decode_output, encode_input, parse_function, tokens_to_json}, @@ -16,7 +16,9 @@ use crate::{ signer::Signer, transaction::{TransactionExecution, TransactionStatusUpdate, submit_and_wait}, }; -pub use gas::{TransactionGas, estimate_function_gas, estimate_native_gas}; +pub use fees::{EvmFeeEstimate, EvmFeeMode}; +pub(crate) use gas::resolve_transaction_gas; +pub use gas::{TransactionGas, TransactionGasPolicy, estimate_function_gas, estimate_native_gas}; #[derive(Clone, Debug)] pub struct CallOutcome { @@ -44,7 +46,13 @@ pub struct CalldataTransaction { pub data: Vec, pub to: Address, pub value: U256, - pub gas: Option, + pub gas: Option, +} + +#[derive(Clone, Debug)] +pub struct CalldataExecution { + pub execution: TransactionExecution, + pub gas: TransactionGas, } pub async fn native_balance(client: &Client, address: Address) -> Result { @@ -164,11 +172,12 @@ pub async fn send_native_with_gas( signer: &S, to: Address, amount: U256, - gas: Option, + gas: Option, on_status: impl FnMut(TransactionStatusUpdate), cancel: impl std::future::Future, ) -> Result { - let tx = prepare_transaction(client, signer.address(), to, Vec::new(), amount, gas).await?; + let (tx, _) = + prepare_transaction(client, signer.address(), to, Vec::new(), amount, gas).await?; submit_transaction(client, signer, tx, on_status, cancel).await } @@ -186,12 +195,12 @@ pub async fn send_function_with_gas( client: &Client, signer: &S, call: FunctionCall<'_>, - gas: Option, + gas: Option, on_status: impl FnMut(TransactionStatusUpdate), cancel: impl std::future::Future, ) -> Result { let data = encode_input(call.function, call.args)?; - let tx = prepare_transaction( + let (tx, _) = prepare_transaction( client, signer.address(), call.contract, @@ -203,14 +212,14 @@ pub async fn send_function_with_gas( submit_transaction(client, signer, tx, on_status, cancel).await } -pub async fn send_calldata_with_gas( +pub async fn send_calldata_with_fee_report( client: &Client, signer: &S, transaction: CalldataTransaction, on_status: impl FnMut(TransactionStatusUpdate), cancel: impl std::future::Future, -) -> Result { - let tx = prepare_transaction( +) -> Result { + let (tx, gas) = prepare_transaction( client, signer.address(), transaction.to, @@ -219,7 +228,8 @@ pub async fn send_calldata_with_gas( transaction.gas, ) .await?; - submit_transaction(client, signer, tx, on_status, cancel).await + let execution = submit_transaction(client, signer, tx, on_status, cancel).await?; + Ok(CalldataExecution { execution, gas }) } pub async fn simulate_calldata( @@ -252,10 +262,11 @@ async fn prepare_transaction( to: Address, data: Vec, value: U256, - gas: Option, -) -> Result { + gas: Option, +) -> Result<(TransactionParameters, TransactionGas)> { let gas = resolve_transaction_gas(client, from, to, &data, value, gas).await?; - fill_transaction(client, from, to, data, value, gas).await + let transaction = fill_transaction(client, from, to, data, value, gas).await?; + Ok((transaction, gas)) } async fn fill_transaction( @@ -277,14 +288,50 @@ async fn fill_transaction( chain_id: Some(chain_id), data: Bytes(data), gas: gas.gas_limit, - gas_price: Some(gas.gas_price), nonce: Some(nonce), to: Some(to), value, - ..Default::default() + ..transaction_fee_parameters(&gas) }) } +fn transaction_fee_parameters(gas: &TransactionGas) -> TransactionParameters { + match &gas.fee { + EvmFeeEstimate::Legacy { gas_price } => TransactionParameters { + gas_price: Some(*gas_price), + ..Default::default() + }, + EvmFeeEstimate::Eip1559 { + max_fee_per_gas, + max_priority_fee_per_gas, + } => TransactionParameters { + transaction_type: Some(U64::from(2)), + max_fee_per_gas: Some(*max_fee_per_gas), + max_priority_fee_per_gas: Some(*max_priority_fee_per_gas), + ..Default::default() + }, + } +} + +pub fn transaction_fee_json(gas: &TransactionGas) -> serde_json::Value { + match &gas.fee { + EvmFeeEstimate::Legacy { gas_price } => serde_json::json!({ + "fee_mode": "legacy", + "gas_price": gas_price.to_string(), + "max_network_fee_wei": gas.max_network_fee().to_string(), + }), + EvmFeeEstimate::Eip1559 { + max_fee_per_gas, + max_priority_fee_per_gas, + } => serde_json::json!({ + "fee_mode": "eip1559", + "max_fee_per_gas": max_fee_per_gas.to_string(), + "max_priority_fee_per_gas": max_priority_fee_per_gas.to_string(), + "max_network_fee_wei": gas.max_network_fee().to_string(), + }), + } +} + async fn submit_transaction( client: &Client, signer: &S, diff --git a/pkg/beam-cli/src/evm/fees.rs b/pkg/beam-cli/src/evm/fees.rs new file mode 100644 index 0000000..e8821f6 --- /dev/null +++ b/pkg/beam-cli/src/evm/fees.rs @@ -0,0 +1,136 @@ +use contextful::ResultContextExt; +use contracts::{Client, U256}; +use web3::types::BlockNumber; + +use crate::error::Result; + +const FEE_HISTORY_BLOCKS: u64 = 20; +const PRIORITY_REWARD_PERCENTILE: f64 = 50.0; +const BASE_FEE_MULTIPLIER: u64 = 2; +const WEI_PER_GWEI: u64 = 1_000_000_000; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EvmFeeMode { + Legacy, + Eip1559, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EvmFeeEstimate { + Legacy { + gas_price: U256, + }, + Eip1559 { + max_fee_per_gas: U256, + max_priority_fee_per_gas: U256, + }, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EvmFeePolicy { + pub base_fee_multiplier: u64, + pub priority_fee_floor: U256, +} + +impl EvmFeeEstimate { + pub fn mode(&self) -> EvmFeeMode { + match self { + Self::Legacy { .. } => EvmFeeMode::Legacy, + Self::Eip1559 { .. } => EvmFeeMode::Eip1559, + } + } + + pub fn max_fee_per_gas(&self) -> U256 { + match self { + Self::Legacy { gas_price } => *gas_price, + Self::Eip1559 { + max_fee_per_gas, .. + } => *max_fee_per_gas, + } + } +} + +pub async fn estimate_fee(client: &Client, chain_id: u64) -> Result { + let policy = EvmFeePolicy { + base_fee_multiplier: BASE_FEE_MULTIPLIER, + priority_fee_floor: priority_fee_floor(chain_id), + }; + + if let Some(estimate) = estimate_eip1559_fee(client, policy).await? { + return Ok(estimate); + } + + let gas_price = client + .fast_gas_price() + .await + .context("fetch beam legacy gas price")?; + Ok(EvmFeeEstimate::Legacy { gas_price }) +} + +async fn estimate_eip1559_fee( + client: &Client, + policy: EvmFeePolicy, +) -> Result> { + let history = match client + .client() + .eth() + .fee_history( + U256::from(FEE_HISTORY_BLOCKS), + BlockNumber::Latest, + Some(vec![PRIORITY_REWARD_PERCENTILE]), + ) + .await + { + Ok(history) => history, + Err(_) => return Ok(None), + }; + + let Some(base_fee) = history + .base_fee_per_gas + .last() + .copied() + .filter(|fee| !fee.is_zero()) + else { + return Ok(None); + }; + + let reward = history + .reward + .unwrap_or_default() + .into_iter() + .filter_map(|row| row.first().copied()) + .filter(|fee| !fee.is_zero()) + .collect::>(); + let priority_fee = std::cmp::max(median_reward(reward), policy.priority_fee_floor); + let max_fee_per_gas = base_fee * U256::from(policy.base_fee_multiplier) + priority_fee; + + Ok(Some(EvmFeeEstimate::Eip1559 { + max_fee_per_gas, + max_priority_fee_per_gas: priority_fee, + })) +} + +fn median_reward(mut rewards: Vec) -> U256 { + if rewards.is_empty() { + return U256::zero(); + } + + rewards.sort_unstable(); + rewards[rewards.len() / 2] +} + +fn priority_fee_floor(chain_id: u64) -> U256 { + match chain_id { + // Ethereum mainnet and Sepolia should not produce dust priority fees. + 1 | 11155111 => gwei(1), + // L2s normally need much lower priority fees than Ethereum mainnet. + 8453 | 42161 => U256::from(1_000_000u64), + // Polygon and BNB need non-dust defaults when they expose EIP-1559 data. + 56 | 137 => gwei(1), + _ => U256::from(10_000_000u64), + } +} + +fn gwei(value: u64) -> U256 { + U256::from(value) * U256::from(WEI_PER_GWEI) +} diff --git a/pkg/beam-cli/src/evm/gas.rs b/pkg/beam-cli/src/evm/gas.rs index 200e3fe..2367251 100644 --- a/pkg/beam-cli/src/evm/gas.rs +++ b/pkg/beam-cli/src/evm/gas.rs @@ -2,18 +2,38 @@ use contextful::ResultContextExt; use contracts::{Address, Client, U256}; use web3::types::{Bytes, CallRequest}; -use super::FunctionCall; -use crate::{abi::encode_input, error::Result}; +use super::{ + FunctionCall, + fees::{EvmFeeEstimate, estimate_fee}, +}; +use crate::{ + abi::encode_input, + error::{Error, Result}, +}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct TransactionGas { pub gas_limit: U256, - pub gas_price: U256, + pub fee: EvmFeeEstimate, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct TransactionGasPolicy { + pub gas_limit: Option, + pub max_network_fee: Option, } impl TransactionGas { pub fn fee(&self) -> U256 { - self.gas_limit * self.gas_price + self.max_network_fee() + } + + pub fn max_network_fee(&self) -> U256 { + self.gas_limit * self.fee.max_fee_per_gas() + } + + pub fn gas_price_for_display(&self) -> U256 { + self.fee.max_fee_per_gas() } } @@ -35,18 +55,35 @@ pub async fn estimate_function_gas( estimate_transaction_gas(client, from, call.contract, &data, call.value).await } -pub(super) async fn resolve_transaction_gas( +pub(crate) async fn resolve_transaction_gas( client: &Client, from: Address, to: Address, data: &[u8], value: U256, - gas: Option, + gas: Option, ) -> Result { - match gas { - Some(gas) => Ok(gas), - None => estimate_transaction_gas(client, from, to, data, value).await, + let gas_policy = gas.unwrap_or_default(); + let gas_limit = match gas_policy.gas_limit { + Some(gas_limit) => pad_gas_limit(gas_limit), + None => estimate_gas_limit(client, from, to, data, value).await?, + }; + let chain_id = client + .chain_id() + .await + .context("fetch beam chain id for fee estimate")? + .as_u64(); + let fee = estimate_fee(client, chain_id).await?; + let resolved = TransactionGas { gas_limit, fee }; + + if let Some(cap) = gas_policy.max_network_fee { + let estimated = resolved.max_network_fee(); + if estimated > cap { + return Err(Error::TransactionFeeCapExceeded { cap, estimated }); + } } + + Ok(resolved) } async fn estimate_transaction_gas( @@ -57,15 +94,14 @@ async fn estimate_transaction_gas( value: U256, ) -> Result { let gas_limit = estimate_gas_limit(client, from, to, data, value).await?; - let gas_price = client - .fast_gas_price() + let chain_id = client + .chain_id() .await - .context("fetch beam gas price")?; + .context("fetch beam chain id for fee estimate")? + .as_u64(); + let fee = estimate_fee(client, chain_id).await?; - Ok(TransactionGas { - gas_limit, - gas_price, - }) + Ok(TransactionGas { gas_limit, fee }) } async fn estimate_gas_limit( @@ -89,5 +125,9 @@ async fn estimate_gas_limit( .await .context("estimate beam transaction gas")?; - Ok(gas + gas / 5) + Ok(pad_gas_limit(gas)) +} + +fn pad_gas_limit(gas: U256) -> U256 { + gas + gas / 5 } diff --git a/pkg/beam-cli/src/tests.rs b/pkg/beam-cli/src/tests.rs index bcbc071..000ece2 100644 --- a/pkg/beam-cli/src/tests.rs +++ b/pkg/beam-cli/src/tests.rs @@ -17,6 +17,7 @@ mod display; mod ens; mod erc20; mod evm; +mod evm_fee; mod evm_gas; mod evm_prepared_gas; mod evm_retries; @@ -33,7 +34,7 @@ mod fetch_retry_origin; mod fetch_test_servers; mod fetch_x402; mod fetch_x402_chain_aliases; -mod fixtures; +pub(crate) mod fixtures; mod inspect; mod interactive; mod interactive_autocomplete; diff --git a/pkg/beam-cli/src/tests/apps.rs b/pkg/beam-cli/src/tests/apps.rs index 647a3c7..6712336 100644 --- a/pkg/beam-cli/src/tests/apps.rs +++ b/pkg/beam-cli/src/tests/apps.rs @@ -56,6 +56,7 @@ fn manifest() -> AppManifest { read_balances: true, propose_transactions: false, erc20_approval: false, + sign_typed_data: false, }, storage: StoragePermission { app_local: true }, privacy: Vec::new(), diff --git a/pkg/beam-cli/src/tests/apps_host.rs b/pkg/beam-cli/src/tests/apps_host.rs index c274600..6f223d7 100644 --- a/pkg/beam-cli/src/tests/apps_host.rs +++ b/pkg/beam-cli/src/tests/apps_host.rs @@ -8,11 +8,12 @@ use crate::{ format_error_chain, host::{ ChainReadOperation, ChainReadRequest, HostTransaction, chain_read, - ensure_chain_read_allowed, ensure_http_allowed, ensure_transaction_allowed, + ensure_app_storage_allowed, ensure_chain_read_allowed, ensure_http_allowed, + ensure_transaction_allowed, }, model::{ ActionBinding, ActionPlan, AppPermissions, ApprovalRecord, ApprovalStatus, - ChainOperation, ChainPermission, HttpPermission, + ChainOperation, ChainPermission, HttpPermission, StoragePermission, }, store::now, }, @@ -57,6 +58,7 @@ fn host_transaction_permissions_enforce_selector_and_spender() { let transaction = HostTransaction { chain: "base".to_string(), data: "0x3593564c".to_string(), + dynamic_contracts: Vec::new(), selector: Some("0x3593564c".to_string()), spender: Some("0xspender".to_string()), target: "0xrouter".to_string(), @@ -88,12 +90,16 @@ fn host_chain_read_permissions_enforce_contract_scope() { address: None, chain: "base".to_string(), data: None, + dynamic_contracts: Vec::new(), + from_block: None, operation: ChainReadOperation::Call, owner: None, selector: Some("0x70a08231".to_string()), spender: None, target: Some("0xrouter".to_string()), token: None, + topics: Vec::new(), + to_block: None, value: None, }; @@ -130,12 +136,16 @@ async fn host_token_metadata_resolves_native_symbol_without_rpc() { address: None, chain: "ethereum".to_string(), data: None, + dynamic_contracts: Vec::new(), + from_block: None, operation: ChainReadOperation::TokenMetadata, owner: None, selector: None, spender: None, target: Some("eth".to_string()), token: Some("eth".to_string()), + topics: Vec::new(), + to_block: None, value: None, }, ) @@ -189,6 +199,7 @@ fn host_transaction_permissions_allow_broad_optional_globs() { let transaction = HostTransaction { chain: "base".to_string(), data: "0xdeadbeef".to_string(), + dynamic_contracts: Vec::new(), selector: Some("0xdeadbeef".to_string()), spender: Some("0xspender".to_string()), target: "0xany".to_string(), @@ -199,6 +210,22 @@ fn host_transaction_permissions_allow_broad_optional_globs() { .expect("omitted optional scopes are broad wildcards"); } +#[test] +fn host_storage_permissions_require_app_local_scope() { + let permissions = AppPermissions { + storage: StoragePermission { app_local: true }, + ..Default::default() + }; + ensure_app_storage_allowed(&permissions).expect("allow declared storage scope"); + + let error = ensure_app_storage_allowed(&AppPermissions::default()) + .expect_err("reject undeclared storage scope"); + assert!(matches!( + error, + apps::Error::StoragePermissionDenied { permission } if permission == "app-local" + )); +} + #[test] fn approval_integrity_rejects_tampered_plan() { let mut plan = action_plan(); @@ -209,6 +236,7 @@ fn approval_integrity_rejects_tampered_plan() { status: ApprovalStatus::Pending, plan, plan_hash, + fee_caps: Vec::new(), created_at: now(), updated_at: now(), }; @@ -258,6 +286,7 @@ fn action_plan() -> ActionPlan { }, ], constraints: Vec::new(), + dynamic_contracts: Vec::new(), expires_at: now() + 60, } } diff --git a/pkg/beam-cli/src/tests/apps_runtime.rs b/pkg/beam-cli/src/tests/apps_runtime.rs index c962db1..f022430 100644 --- a/pkg/beam-cli/src/tests/apps_runtime.rs +++ b/pkg/beam-cli/src/tests/apps_runtime.rs @@ -4,7 +4,7 @@ use super::fixtures::{test_app, test_app_with_output}; use crate::{ apps::{ Error, - model::{AppManifest, InstalledApp, RegistryIndex}, + model::{AppManifest, InstalledApp, RegistryIndex, RegistryVersion}, runtime::{AppRuntime, validate_wasm_module}, store::{AppCache, now}, }, @@ -26,8 +26,7 @@ const WASM_WITHOUT_COMMAND_ALLOC: &[u8] = b"\0asm\x01\0\0\0\ #[test] fn app_runtime_requires_declared_entrypoint() { let bundle = repo_root().join("beam-apps/fixtures/valid"); - let index = read_json::(&bundle.join("index.json")); - let version = &index.apps[0].versions[0]; + let version = uniswap_fixture_version(&bundle); let path = artifact_path(&bundle, &version.module_url); validate_wasm_module("uniswap", "beam_app_main", &path).expect("valid app wasm"); @@ -54,8 +53,7 @@ async fn app_command_help_skips_stale_wasm_validation() { let (_temp_dir, app) = test_app_with_output(OutputMode::Quiet, InvocationOverrides::default()).await; let bundle = repo_root().join("beam-apps/fixtures/valid"); - let index = read_json::(&bundle.join("index.json")); - let version = &index.apps[0].versions[0]; + let version = uniswap_fixture_version(&bundle); let manifest_path = artifact_path(&bundle, &version.manifest_url); let manifest_bytes = std::fs::read(&manifest_path).expect("read manifest"); let manifest = read_json::(&manifest_path); @@ -80,6 +78,7 @@ async fn app_command_help_skips_stale_wasm_validation() { app: "uniswap".to_string(), prepare: false, no_prompt: false, + max_network_fee_wei: None, args: vec!["swap".to_string(), "--help".to_string()], }, ) @@ -92,8 +91,7 @@ async fn app_run_checks_installed_manifest_minimum_version_before_wasm() { let (_temp_dir, app) = test_app_with_output(OutputMode::Quiet, InvocationOverrides::default()).await; let bundle = repo_root().join("beam-apps/fixtures/valid"); - let index = read_json::(&bundle.join("index.json")); - let version = &index.apps[0].versions[0]; + let version = uniswap_fixture_version(&bundle); let manifest_path = artifact_path(&bundle, &version.manifest_url); let mut manifest = read_json::(&manifest_path); manifest.min_beam_version = "999.0.0".to_string(); @@ -119,6 +117,7 @@ async fn app_run_checks_installed_manifest_minimum_version_before_wasm() { app: "uniswap".to_string(), prepare: false, no_prompt: false, + max_network_fee_wei: None, args: vec!["unknown".to_string()], }, ) @@ -141,14 +140,13 @@ async fn app_runtime_invokes_guest_and_returns_structured_errors() { }) .await; let bundle = repo_root().join("beam-apps/fixtures/valid"); - let index = read_json::(&bundle.join("index.json")); - let version = &index.apps[0].versions[0]; + let version = uniswap_fixture_version(&bundle); let manifest_path = artifact_path(&bundle, &version.manifest_url); let module_path = artifact_path(&bundle, &version.module_url); let manifest = read_json(&manifest_path); let installed = InstalledApp { active_version: version.version.clone(), - id: index.apps[0].id.clone(), + id: "uniswap".to_string(), installed_at: now(), manifest_sha256: version.manifest_sha256.clone(), module_sha256: version.module_sha256.clone(), @@ -172,6 +170,17 @@ fn repo_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") } +fn uniswap_fixture_version(bundle: &Path) -> RegistryVersion { + let index = read_json::(&bundle.join("index.json")); + index + .apps + .iter() + .find(|app| app.id == "uniswap") + .expect("find uniswap fixture") + .versions[0] + .clone() +} + fn read_json(path: &Path) -> T { serde_json::from_slice(&std::fs::read(path).expect("read json")).expect("decode json") } diff --git a/pkg/beam-cli/src/tests/evm.rs b/pkg/beam-cli/src/tests/evm.rs index 7fbff3b..6d763de 100644 --- a/pkg/beam-cli/src/tests/evm.rs +++ b/pkg/beam-cli/src/tests/evm.rs @@ -1,4 +1,4 @@ -// lint-long-file-override allow-max-lines=300 +// lint-long-file-override allow-max-lines=400 use std::{ future::pending, sync::{Arc, Mutex}, @@ -103,7 +103,8 @@ async fn native_transfers_estimate_gas_before_submission() { methods, vec![ "eth_estimateGas", - "eth_gasPrice", + "eth_chainId", + "eth_feeHistory", "eth_getTransactionCount", "eth_chainId", "eth_sendRawTransaction", @@ -163,22 +164,23 @@ async fn native_transfers_return_pending_hash_when_wait_is_cancelled() { let calls = calls.lock().expect("rpc calls").clone(); let methods = rpc_methods(&calls); assert_eq!( - &methods[..5], + &methods[..6], &[ "eth_estimateGas", - "eth_gasPrice", + "eth_chainId", + "eth_feeHistory", "eth_getTransactionCount", "eth_chainId", "eth_sendRawTransaction", ] ); - if methods.len() == 7 { + if methods.len() == 8 { assert_eq!( - &methods[5..], + &methods[6..], &["eth_getTransactionReceipt", "eth_getTransactionByHash"] ); } else { - assert_eq!(methods.len(), 5); + assert_eq!(methods.len(), 6); } } @@ -269,6 +271,12 @@ fn rpc_response(request: &Value, scenario: RpcScenario) -> String { "eth_gasPrice" => serde_json::to_value(U256::from(1_000_000_000u64)).expect("gas price"), "eth_getTransactionCount" => serde_json::to_value(U256::zero()).expect("nonce"), "eth_chainId" => serde_json::to_value(U256::one()).expect("chain id"), + "eth_feeHistory" => json!({ + "oldestBlock": "0x1", + "baseFeePerGas": ["0x3b9aca00", "0x3b9aca00"], + "gasUsedRatio": [0.5], + "reward": [["0x3b9aca00"]], + }), "eth_sendRawTransaction" => serde_json::to_value(H256::from_low_u64_be(7)).expect("hash"), "eth_getTransactionReceipt" => match scenario { RpcScenario::Confirmed => { diff --git a/pkg/beam-cli/src/tests/evm_fee.rs b/pkg/beam-cli/src/tests/evm_fee.rs new file mode 100644 index 0000000..f81f97d --- /dev/null +++ b/pkg/beam-cli/src/tests/evm_fee.rs @@ -0,0 +1,189 @@ +use std::sync::{Arc, Mutex}; + +use contracts::{Address, Client, U256}; +use serde_json::{Value, json}; +use tokio::{ + io::AsyncWriteExt, + net::{TcpListener, TcpStream}, +}; + +use super::fixtures::read_rpc_request; +use crate::evm::estimate_native_gas; + +#[tokio::test] +async fn eip1559_fee_estimation_uses_priority_floor_for_weak_rewards() { + let (rpc_url, calls, server) = spawn_fee_rpc_server(FeeScenario::WeakReward).await; + let client = Client::try_new(&rpc_url, None).expect("create client"); + + let gas = estimate_native_gas( + &client, + Address::from_low_u64_be(1), + Address::from_low_u64_be(2), + U256::zero(), + ) + .await + .expect("estimate gas"); + server.abort(); + + assert_eq!(gas.gas_limit, U256::from(36_000u64)); + assert_eq!(gas.gas_price_for_display(), U256::from(3_000_000_000u64)); + assert_eq!( + rpc_methods(&calls.lock().expect("rpc calls")), + vec!["eth_estimateGas", "eth_chainId", "eth_feeHistory"] + ); +} + +#[tokio::test] +async fn eip1559_fee_estimation_uses_floor_when_rewards_are_missing() { + let (rpc_url, _calls, server) = spawn_fee_rpc_server(FeeScenario::MissingReward).await; + let client = Client::try_new(&rpc_url, None).expect("create client"); + + let gas = estimate_native_gas( + &client, + Address::from_low_u64_be(1), + Address::from_low_u64_be(2), + U256::zero(), + ) + .await + .expect("estimate gas"); + server.abort(); + + assert_eq!(gas.gas_price_for_display(), U256::from(2_001_000_000u64)); +} + +#[tokio::test] +async fn fee_estimation_falls_back_to_legacy_when_fee_history_is_missing() { + let (rpc_url, calls, server) = spawn_fee_rpc_server(FeeScenario::NoFeeHistory).await; + let client = Client::try_new(&rpc_url, None).expect("create client"); + + let gas = estimate_native_gas( + &client, + Address::from_low_u64_be(1), + Address::from_low_u64_be(2), + U256::zero(), + ) + .await + .expect("estimate gas"); + server.abort(); + + assert_eq!(gas.gas_price_for_display(), U256::from(1_100_000_000u64)); + assert_eq!( + rpc_methods(&calls.lock().expect("rpc calls")), + vec![ + "eth_estimateGas", + "eth_chainId", + "eth_feeHistory", + "eth_gasPrice", + ] + ); +} + +#[derive(Clone, Copy)] +enum FeeScenario { + WeakReward, + MissingReward, + NoFeeHistory, +} + +async fn spawn_fee_rpc_server( + scenario: FeeScenario, +) -> (String, Arc>>, tokio::task::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind fee rpc listener"); + let address = listener.local_addr().expect("listener address"); + let calls = Arc::new(Mutex::new(Vec::new())); + let server_calls = Arc::clone(&calls); + + let server = tokio::spawn(async move { + loop { + let (stream, _peer) = listener.accept().await.expect("accept rpc connection"); + handle_fee_rpc_connection(stream, Arc::clone(&server_calls), scenario).await; + } + }); + + (format!("http://{address}"), calls, server) +} + +async fn handle_fee_rpc_connection( + mut stream: TcpStream, + calls: Arc>>, + scenario: FeeScenario, +) { + let request = read_rpc_request(&mut stream).await; + calls + .lock() + .expect("record rpc request") + .push(request.clone()); + + let body = rpc_response(&request, scenario); + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", + body.len(), + body + ); + stream + .write_all(response.as_bytes()) + .await + .expect("write rpc response"); +} + +fn rpc_response(request: &Value, scenario: FeeScenario) -> String { + if request["method"] == "eth_feeHistory" && matches!(scenario, FeeScenario::NoFeeHistory) { + return json!({ + "jsonrpc": "2.0", + "id": request["id"].clone(), + "error": { + "code": -32601, + "message": "method not found", + }, + }) + .to_string(); + } + + let result = match request["method"].as_str().expect("rpc method") { + "eth_estimateGas" => serde_json::to_value(U256::from(30_000u64)).expect("estimate gas"), + "eth_chainId" => serde_json::to_value(chain_id(scenario)).expect("chain id"), + "eth_feeHistory" => fee_history(scenario), + "eth_gasPrice" => serde_json::to_value(U256::from(1_000_000_000u64)).expect("gas price"), + other => panic!("unexpected rpc method {other}"), + }; + + json!({ + "jsonrpc": "2.0", + "id": request["id"].clone(), + "result": result, + }) + .to_string() +} + +fn fee_history(scenario: FeeScenario) -> Value { + match scenario { + FeeScenario::WeakReward => json!({ + "oldestBlock": "0x1", + "baseFeePerGas": ["0x3b9aca00", "0x3b9aca00"], + "gasUsedRatio": [0.5], + "reward": [["0x1"]], + }), + FeeScenario::MissingReward => json!({ + "oldestBlock": "0x1", + "baseFeePerGas": ["0x3b9aca00", "0x3b9aca00"], + "gasUsedRatio": [0.5], + }), + FeeScenario::NoFeeHistory => unreachable!("handled before result response"), + } +} + +fn chain_id(scenario: FeeScenario) -> U256 { + match scenario { + FeeScenario::WeakReward | FeeScenario::NoFeeHistory => U256::one(), + FeeScenario::MissingReward => U256::from(8453u64), + } +} + +fn rpc_methods(calls: &[Value]) -> Vec<&str> { + calls + .iter() + .map(|call| call["method"].as_str().expect("rpc method")) + .collect() +} diff --git a/pkg/beam-cli/src/tests/evm_gas.rs b/pkg/beam-cli/src/tests/evm_gas.rs index 98d22de..6935fb6 100644 --- a/pkg/beam-cli/src/tests/evm_gas.rs +++ b/pkg/beam-cli/src/tests/evm_gas.rs @@ -36,10 +36,13 @@ async fn function_gas_estimation_encodes_call_without_submission() { server.abort(); assert_eq!(gas.gas_limit, U256::from(36_000u64)); - assert_eq!(gas.gas_price, U256::from(1_100_000_000u64)); + assert_eq!(gas.gas_price_for_display(), U256::from(3_000_000_000u64)); let calls = calls.lock().expect("rpc calls").clone(); - assert_eq!(rpc_methods(&calls), vec!["eth_estimateGas", "eth_gasPrice"]); + assert_eq!( + rpc_methods(&calls), + vec!["eth_estimateGas", "eth_chainId", "eth_feeHistory"] + ); let estimate = &calls[0]["params"][0]; assert_eq!(estimate["from"], Value::String(format!("{from:#x}"))); assert_eq!(estimate["to"], Value::String(format!("{contract:#x}"))); diff --git a/pkg/beam-cli/src/tests/evm_prepared_gas.rs b/pkg/beam-cli/src/tests/evm_prepared_gas.rs index f15c85a..9e34b1a 100644 --- a/pkg/beam-cli/src/tests/evm_prepared_gas.rs +++ b/pkg/beam-cli/src/tests/evm_prepared_gas.rs @@ -17,13 +17,13 @@ use web3::{ use super::fixtures::read_rpc_request; use crate::{ abi::parse_function, - evm::{FunctionCall, TransactionGas, send_function_with_gas, send_native_with_gas}, + evm::{FunctionCall, TransactionGasPolicy, send_function_with_gas, send_native_with_gas}, signer::KeySigner, transaction::TransactionExecution, }; #[tokio::test] -async fn native_transfers_with_prepared_gas_skip_reestimation() { +async fn native_transfers_with_prepared_gas_skip_gas_limit_reestimation() { let (rpc_url, calls, server) = spawn_prepared_gas_rpc_server().await; let client = Client::try_new(&rpc_url, None).expect("create client"); let signer = KeySigner::from_slice(&[7u8; 32]).expect("create signer"); @@ -47,6 +47,8 @@ async fn native_transfers_with_prepared_gas_skip_reestimation() { assert_eq!( rpc_methods(&calls.lock().expect("rpc calls")), vec![ + "eth_chainId", + "eth_feeHistory", "eth_getTransactionCount", "eth_chainId", "eth_sendRawTransaction", @@ -56,7 +58,7 @@ async fn native_transfers_with_prepared_gas_skip_reestimation() { } #[tokio::test] -async fn function_calls_with_prepared_gas_skip_reestimation() { +async fn function_calls_with_prepared_gas_skip_gas_limit_reestimation() { let (rpc_url, calls, server) = spawn_prepared_gas_rpc_server().await; let client = Client::try_new(&rpc_url, None).expect("create client"); let signer = KeySigner::from_slice(&[7u8; 32]).expect("create signer"); @@ -90,6 +92,8 @@ async fn function_calls_with_prepared_gas_skip_reestimation() { assert_eq!( rpc_methods(&calls.lock().expect("rpc calls")), vec![ + "eth_chainId", + "eth_feeHistory", "eth_getTransactionCount", "eth_chainId", "eth_sendRawTransaction", @@ -98,10 +102,10 @@ async fn function_calls_with_prepared_gas_skip_reestimation() { ); } -fn prepared_gas() -> TransactionGas { - TransactionGas { - gas_limit: U256::from(36_000u64), - gas_price: U256::from(1_000_000_000u64), +fn prepared_gas() -> TransactionGasPolicy { + TransactionGasPolicy { + gas_limit: Some(U256::from(36_000u64)), + max_network_fee: Some(U256::from(1_000_000_000_000_000u64)), } } @@ -154,6 +158,12 @@ fn rpc_response(request: &Value) -> String { let result = match request["method"].as_str().expect("rpc method") { "eth_getTransactionCount" => serde_json::to_value(U256::zero()).expect("nonce"), "eth_chainId" => serde_json::to_value(U256::one()).expect("chain id"), + "eth_feeHistory" => json!({ + "oldestBlock": "0x1", + "baseFeePerGas": ["0x3b9aca00", "0x3b9aca00"], + "gasUsedRatio": [0.5], + "reward": [["0x3b9aca00"]], + }), "eth_sendRawTransaction" => serde_json::to_value(H256::from_low_u64_be(7)).expect("hash"), "eth_getTransactionReceipt" => serde_json::to_value(successful_receipt()).expect("receipt"), other => panic!("unexpected rpc method {other}"), diff --git a/pkg/beam-cli/src/tests/evm_retries.rs b/pkg/beam-cli/src/tests/evm_retries.rs index 790d885..99f20ed 100644 --- a/pkg/beam-cli/src/tests/evm_retries.rs +++ b/pkg/beam-cli/src/tests/evm_retries.rs @@ -81,11 +81,12 @@ async fn send_native_retries_transient_estimate_gas_failures() { assert_eq!(method_count(&state, "eth_estimateGas"), 2); assert_eq!(method_count(&state, "eth_sendRawTransaction"), 1); assert_eq!( - rpc_methods(&state)[..6], + rpc_methods(&state)[..7], [ "eth_estimateGas", "eth_estimateGas", - "eth_gasPrice", + "eth_chainId", + "eth_feeHistory", "eth_getTransactionCount", "eth_chainId", "eth_sendRawTransaction", @@ -123,10 +124,11 @@ async fn send_native_retries_transient_raw_submission_failures() { assert_eq!(method_count(&state, "eth_estimateGas"), 1); assert_eq!(method_count(&state, "eth_sendRawTransaction"), 2); assert_eq!( - rpc_methods(&state)[..7], + rpc_methods(&state)[..8], [ "eth_estimateGas", - "eth_gasPrice", + "eth_chainId", + "eth_feeHistory", "eth_getTransactionCount", "eth_chainId", "eth_sendRawTransaction", @@ -221,6 +223,12 @@ fn rpc_response(request: &Value, mode: RetryRpcMode) -> String { (RetryRpcMode::ConfirmedTransfer, "eth_gasPrice") => { serde_json::to_value(U256::from(1_000_000_000u64)).expect("gas price") } + (RetryRpcMode::ConfirmedTransfer, "eth_feeHistory") => json!({ + "oldestBlock": "0x1", + "baseFeePerGas": ["0x3b9aca00", "0x3b9aca00"], + "gasUsedRatio": [0.5], + "reward": [["0x3b9aca00"]], + }), (RetryRpcMode::ConfirmedTransfer, "eth_getTransactionCount") => { serde_json::to_value(U256::zero()).expect("nonce") } diff --git a/pkg/beam-cli/src/tests/fixtures.rs b/pkg/beam-cli/src/tests/fixtures.rs index c320351..3fc84b1 100644 --- a/pkg/beam-cli/src/tests/fixtures.rs +++ b/pkg/beam-cli/src/tests/fixtures.rs @@ -15,7 +15,7 @@ pub(super) async fn test_app(overrides: InvocationOverrides) -> (TempDir, BeamAp test_app_with_output(OutputMode::Default, overrides).await } -pub(super) async fn test_app_with_output( +pub(crate) async fn test_app_with_output( output_mode: OutputMode, overrides: InvocationOverrides, ) -> (TempDir, BeamApp) { @@ -68,7 +68,7 @@ async fn serve_chain_id_connection(mut stream: TcpStream, chain_id: u64) { .expect("write rpc response"); } -pub(super) async fn read_rpc_request(stream: &mut TcpStream) -> Value { +pub(crate) async fn read_rpc_request(stream: &mut TcpStream) -> Value { let mut buffer = Vec::new(); let body_offset = loop { let mut chunk = [0u8; 1024];