diff --git a/Cargo.lock b/Cargo.lock index 02c9770..810bf1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13985,6 +13985,7 @@ name = "workspace-hack" version = "0.1.0" dependencies = [ "actix-router", + "aead", "ahash", "aho-corasick", "allocator-api2", @@ -14015,6 +14016,7 @@ dependencies = [ "ark-std 0.5.0", "arrayvec", "async-compression", + "axum", "base64 0.13.1", "bindgen 0.71.1", "bitflags 2.10.0", @@ -14043,6 +14045,7 @@ dependencies = [ "darling_core 0.21.3", "dashmap", "data-encoding", + "der", "derive_more 2.1.1", "derive_more-impl 2.1.1", "digest 0.10.7", @@ -14114,6 +14117,7 @@ dependencies = [ "once_cell", "openssl", "openssl-sys", + "p256", "parity-scale-codec", "parking_lot 0.12.5", "percent-encoding", @@ -14166,6 +14170,7 @@ dependencies = [ "socket2 0.5.10", "socket2 0.6.1", "spin 0.9.8", + "spki", "strum 0.27.2", "subtle", "syn 1.0.109", @@ -14198,6 +14203,7 @@ dependencies = [ "windows-sys 0.60.2", "windows-sys 0.61.2", "winnow 0.7.14", + "x25519-dalek 2.0.1", "zeroize", "zstd", "zstd-safe", diff --git a/Cargo.toml b/Cargo.toml index b97a6d8..5ae4f4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,8 @@ test-spy-macros = { path = "./pkg/test-spy-macros" } block-store = { path = "./pkg/block-store" } contracts = { path = "./pkg/contracts" } constants = { path = "./pkg/constants" } +contract-test = { path = "./pkg/contract-test" } +contract-test-macro = { path = "./pkg/contract-test-macro" } country = { path = "./pkg/country" } client-http = { path = "./pkg/client-http" } client-http-longpoll = { path = "./pkg/client-http-longpoll" } @@ -70,6 +72,14 @@ hash-poseidon = { path = "./pkg/hash-poseidon" } hmac-sha256-json = { path = "./pkg/hmac-sha256-json" } json-store = { path = "./pkg/json-store" } json-with-logging = { path = "./pkg/json-with-logging" } +jws-es256-interface = { path = "./pkg/jws-es256-interface" } +jws-es256-p256 = { path = "./pkg/jws-es256-p256" } +time-interface = { path = "./pkg/time-interface" } +time-system = { path = "./pkg/time-system" } +email-interface = { path = "./pkg/email-interface" } +email-memory = { path = "./pkg/email-memory" } +evm-json-rpc-interface = { path = "./pkg/evm-json-rpc-interface" } +evm-json-rpc-reqwest = { path = "./pkg/evm-json-rpc-reqwest" } serde_yaml = { path = "./pkg/serde_yaml" } node = { path = "./pkg/node" } node-client-http = { path = "./pkg/node-client-http" } @@ -102,6 +112,56 @@ posthog-interface = { path = "./pkg/posthog-interface" } posthog = { path = "./pkg/posthog" } kyc = { path = "./pkg/kyc" } primitives = { path = "./pkg/primitives" } +payy-auth-embedded-wallet-document = { path = "./pkg/payy-auth-embedded-wallet-document" } +payy-auth-app-admission-interface = { path = "./pkg/payy-auth-app-admission-interface" } +payy-auth-app-admission-catalog = { path = "./pkg/payy-auth-app-admission-catalog" } +payy-auth-app-catalog-interface = { path = "./pkg/payy-auth-app-catalog-interface" } +payy-auth-app-catalog-memory = { path = "./pkg/payy-auth-app-catalog-memory" } +payy-auth-app-catalog-server = { path = "./pkg/payy-auth-app-catalog-server" } +payy-auth-app-catalog-test-support = { path = "./pkg/payy-auth-app-catalog-test-support" } +payy-auth-app-client-scope-interface = { path = "./pkg/payy-auth-app-client-scope-interface" } +payy-auth-app-client-scope = { path = "./pkg/payy-auth-app-client-scope" } +payy-auth-client-access-interface = { path = "./pkg/payy-auth-client-access-interface" } +payy-auth-client-access-catalog = { path = "./pkg/payy-auth-client-access-catalog" } +payy-auth-client-access-test-support = { path = "./pkg/payy-auth-client-access-test-support" } +payy-auth-user-interface = { path = "./pkg/payy-auth-user-interface" } +payy-auth-session-interface = { path = "./pkg/payy-auth-session-interface" } +payy-auth-login-commit-interface = { path = "./pkg/payy-auth-login-commit-interface" } +payy-auth-login-commit-policy-catalog = { path = "./pkg/payy-auth-login-commit-policy-catalog" } +payy-auth-login-commit-test-support = { path = "./pkg/payy-auth-login-commit-test-support" } +payy-auth-refresh-session-core = { path = "./pkg/payy-auth-refresh-session-core" } +payy-auth-passwordless-interface = { path = "./pkg/payy-auth-passwordless-interface" } +payy-auth-passwordless-test-support = { path = "./pkg/payy-auth-passwordless-test-support" } +payy-auth-passwordless = { path = "./pkg/payy-auth-passwordless" } +payy-auth-passwordless-code-os-rng = { path = "./pkg/payy-auth-passwordless-code-os-rng" } +payy-auth-passwordless-challenge-memory = { path = "./pkg/payy-auth-passwordless-challenge-memory" } +payy-auth-passwordless-rate-limit-allow-all = { path = "./pkg/payy-auth-passwordless-rate-limit-allow-all" } +payy-auth-passwordless-policy-catalog = { path = "./pkg/payy-auth-passwordless-policy-catalog" } +payy-auth-passwordless-fail-closed = { path = "./pkg/payy-auth-passwordless-fail-closed" } +payy-auth-passwordless-email = { path = "./pkg/payy-auth-passwordless-email" } +payy-auth-session-memory = { path = "./pkg/payy-auth-session-memory" } +payy-auth-api-request-interface = { path = "./pkg/payy-auth-api-request-interface" } +payy-auth-api-gate = { path = "./pkg/payy-auth-api-gate" } +payy-auth-wallet-interface = { path = "./pkg/payy-auth-wallet-interface" } +payy-auth-wallet = { path = "./pkg/payy-auth-wallet" } +payy-auth-wallet-p256 = { path = "./pkg/payy-auth-wallet-p256" } +payy-auth-wallet-custody-local = { path = "./pkg/payy-auth-wallet-custody-local" } +payy-auth-wallet-storage-core = { path = "./pkg/payy-auth-wallet-storage-core" } +payy-auth-wallet-storage-json = { path = "./pkg/payy-auth-wallet-storage-json" } +payy-auth-wallet-storage-memory = { path = "./pkg/payy-auth-wallet-storage-memory" } +payy-auth-wallet-test-support = { path = "./pkg/payy-auth-wallet-test-support" } +payy-auth-wallet-transaction-evm-json-rpc = { path = "./pkg/payy-auth-wallet-transaction-evm-json-rpc" } +payy-auth-api-bin = { path = "./pkg/payy-auth-api-bin" } +payy-auth-api-http = { path = "./pkg/payy-auth-api-http" } +payy-auth-api-interface = { path = "./pkg/payy-auth-api-interface" } +payy-auth-api-test-support = { path = "./pkg/payy-auth-api-test-support" } +payy-auth-local = { path = "./pkg/payy-auth-local" } +payy-auth-runtime-system = { path = "./pkg/payy-auth-runtime-system" } +payy-auth-session = { path = "./pkg/payy-auth-session" } +payy-auth-session-test-support = { path = "./pkg/payy-auth-session-test-support" } +payy-auth-session-storage-memory = { path = "./pkg/payy-auth-session-storage-memory" } +payy-auth-backend-interface = { path = "./pkg/payy-auth-backend-interface" } +payy-auth-api-server = { path = "./pkg/payy-auth-api-server" } prover = { path = "./pkg/prover" } providers-interface = { path = "./pkg/providers-interface" } rpc = { path = "./pkg/rpc" } @@ -123,6 +183,7 @@ eip7702 = { path = "./pkg/eip7702" } payy_core = { path = "./pkg/payy_core" } payy_core_types = { path = "./pkg/payy_core_types" } payy-app-interface = { path = "./pkg/payy-app-interface" } +payy-auth-app-interface = { path = "./pkg/payy-auth-app-interface" } payy-app-wallet-mobile = { path = "./pkg/payy-app-wallet-mobile" } payy-note = { path = "./pkg/payy-note" } payy-evm-parse-link = { path = "./pkg/payy-evm-parse-link" } @@ -262,6 +323,7 @@ futures-timer = "3.0.2" futures-util = "0.3.29" halo2curves = "0.1.0" hex = { version = "0.4", features = ["serde"] } +hpke = "0.13.0" hmac = "0.12" home = "0.5.11" indoc = "2" @@ -288,7 +350,9 @@ num-bigint = "0.4.6" num-traits = "0.2" openssl = { version = "0.10.75", default-features = false } once_cell = "1.19.0" +p256 = { version = "0.13.2", features = ["pkcs8", "pem"] } parking_lot = "0.12.1" +percent-encoding = "2.3.2" phonenumber = "0.3" pretty-hex = "0.3.0" proptest = "1.11.0" @@ -297,6 +361,7 @@ quote = "1.0" quickcheck = "1.0.3" rand = "0.8.5" rand_chacha = "0.3.1" +rand_chacha_09 = { package = "rand_chacha", version = "0.9.0" } rand_xorshift = "0.4" reqwest = { version = "0.12", features = ["json", "multipart", "stream"] } rlp = "0.6.1" @@ -310,6 +375,7 @@ sentry = "0.46.0" sentry-tracing = "0.46.2" serde = { version = "1.0.221", features = ["derive"] } serde_json = "1.0.145" +serde_json_canonicalizer = "0.3.2" serde_urlencoded = "0.7.1" serde_qs = "0.15.0" serde_bytes = "0.11.19" @@ -326,6 +392,7 @@ sha1 = "0.10.1" sha2 = "0.10.6" sha3 = "0.10.1" zstd = "0.13.3" +zeroize = "1" self-replace = "1.5.0" spinoff = "0.8.0" syn = { version = "2.0", features = ["full", "extra-traits"] } diff --git a/beam-apps/README.md b/beam-apps/README.md index d1e3f7d..364ce0e 100644 --- a/beam-apps/README.md +++ b/beam-apps/README.md @@ -26,6 +26,11 @@ scripts/beam-app-registry/build.py scripts/beam-app-registry/verify.py ``` +The build step compiles command-capable app WASM and validates the Beam command +ABI before writing a bundle. A release artifact is rejected if it does not export +`memory`, `beam_alloc`, `beam_free`, and the manifest entrypoint, or if it does +not import `env.beam_host_call`. + Local registry server: ```bash diff --git a/beam-apps/apps/uniswap/README.md b/beam-apps/apps/uniswap/README.md index 0206a19..0da5a32 100644 --- a/beam-apps/apps/uniswap/README.md +++ b/beam-apps/apps/uniswap/README.md @@ -28,6 +28,13 @@ beam apps install uniswap --dry-run beam x uniswap swap [options] ``` +Command help is exported through the app manifest and rendered by Beam without +fetching a quote or invoking guest WASM: + +```bash +beam x uniswap swap --help +``` + Example: ```bash @@ -48,6 +55,9 @@ approve the final plan before Beam signs or submits anything. - `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval instead of the default exact approval. +Token inputs can be Beam token labels, `native`, the active chain's native +symbol, or EVM token addresses. + ## How a Swap Works 1. The app fetches a quote through Beam-mediated HTTPS access. @@ -57,6 +67,12 @@ approve the final plan before Beam signs or submits anything. 5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the receipt. +If an approval is required, Beam submits it first and submits the swap only after +the approval is confirmed. If fresh allowance already satisfies the exact plan, +Beam skips the approval step. Execution output reports confirmed, pending, +dropped, or skipped transaction state; confirmed receipts include the RPC status +value. + ## Agents Agents and other non-interactive callers should prepare a continuation, inspect diff --git a/beam-apps/apps/uniswap/src/error.rs b/beam-apps/apps/uniswap/src/error.rs index 0d95f78..0050b3d 100644 --- a/beam-apps/apps/uniswap/src/error.rs +++ b/beam-apps/apps/uniswap/src/error.rs @@ -25,4 +25,13 @@ pub enum Error { #[error("[beam-app-uniswap] address value is invalid: {value}")] InvalidAddress { value: String }, + + #[error("[beam-app-uniswap] host call failed: {message}")] + HostCallFailed { message: String }, + + #[error("[beam-app-uniswap] host response is invalid: {reason}")] + InvalidHostResponse { reason: String }, + + #[error("[beam-app-uniswap] serialization failed: {reason}")] + Serialization { reason: String }, } diff --git a/beam-apps/apps/uniswap/src/host.rs b/beam-apps/apps/uniswap/src/host.rs index 1b9143e..a86fb5c 100644 --- a/beam-apps/apps/uniswap/src/host.rs +++ b/beam-apps/apps/uniswap/src/host.rs @@ -1,5 +1,11 @@ +// lint-long-file-override allow-max-lines=450 use serde_json::Value; +use crate::{Error, Result, selector}; + +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, @@ -18,6 +24,40 @@ pub struct SwapToken { pub label: 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, + 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, @@ -53,3 +93,391 @@ 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), + SimulateTransaction(HostTransaction), + StructuredOutput { value: Value }, + Diagnostic { level: String, message: String }, + ResolveAddress { value: Option }, + AppStorageGet { key: String }, + AppStorageSet { key: String, value: Value }, + AppStorageRemove { key: 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)] +struct HttpFetchResponse { + body: Vec, + status: u16, + url: String, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct ChainReadRequest { + chain: String, + operation: ChainReadOperation, + address: Option, + data: Option, + owner: Option, + spender: Option, + target: Option, + token: Option, + value: Option, + selector: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +enum ChainReadOperation { + TokenMetadata, + Balance, + Allowance, + Gas, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +struct HostTransaction { + chain: String, + data: String, + target: String, + value: String, + selector: Option, + spender: Option, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct HostCallResponse { + ok: bool, + value: Option, + error: Option, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct TokenMetadataResponse { + address: String, + decimals: Option, + label: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct BalanceResponse { + balance: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct AllowanceResponse { + allowance: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct GasResponse { + gas_estimate: Option, + gas_price: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct ResolveAddressResponse { + address: String, +} + +#[derive(Clone, Debug, serde::Deserialize)] +struct StorageGetResponse { + exists: bool, + value: Option, +} + +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 http_json(method: &str, url: &str, value: &Value) -> Result { + let body = serde_json::to_vec(value).map_err(|err| Error::Serialization { + reason: err.to_string(), + })?; + let response = host_call(HostRequest::HttpFetch(HttpFetchRequest { + method: method.to_string(), + url: url.to_string(), + headers: vec![ + HttpHeader { + name: "content-type".to_string(), + value: "application/json".to_string(), + }, + HttpHeader { + name: "x-api-key".to_string(), + value: crate::public_api_key().to_string(), + }, + ], + body, + }))?; + let response = serde_json::from_value::(response).map_err(|err| { + Error::InvalidHostResponse { + reason: err.to_string(), + } + })?; + if !(200..300).contains(&response.status) { + return Err(Error::HostCallFailed { + message: format!("{} returned status {}", response.url, response.status), + }); + } + serde_json::from_slice(&response.body).map_err(|err| Error::InvalidHostResponse { + reason: err.to_string(), + }) +} + +pub fn resolve_address(value: Option<&str>) -> Result { + let response = host_call(HostRequest::ResolveAddress { + value: value.map(str::to_string), + })?; + Ok(parse_host_value::(response)?.address) +} + +pub fn token_metadata(chain: &str, token: &str) -> Result { + let response = chain_read(ChainReadRequest { + address: None, + chain: chain.to_string(), + data: None, + operation: ChainReadOperation::TokenMetadata, + owner: None, + selector: None, + spender: None, + target: Some(token.to_string()), + token: Some(token.to_string()), + value: None, + })?; + let response = parse_host_value::(response)?; + let decimals = response.decimals.ok_or_else(|| Error::InvalidHostResponse { + reason: format!("token {token} missing decimals"), + })?; + Ok(SwapToken { + is_native: is_native_token(&response.address, &response.label), + address: response.address, + decimals, + label: response.label, + }) +} + +pub fn balance(chain: &str, token: &str) -> Result { + let response = chain_read(ChainReadRequest { + address: None, + chain: chain.to_string(), + data: None, + operation: ChainReadOperation::Balance, + owner: None, + selector: None, + spender: None, + target: None, + token: Some(token.to_string()), + value: None, + })?; + Ok(parse_host_value::(response)?.balance) +} + +pub fn allowance(chain: &str, token: &str, spender: &str) -> Result { + let response = chain_read(ChainReadRequest { + address: None, + chain: chain.to_string(), + data: None, + operation: ChainReadOperation::Allowance, + owner: None, + selector: None, + spender: Some(spender.to_string()), + target: Some(token.to_string()), + token: Some(token.to_string()), + value: None, + })?; + Ok(parse_host_value::(response)?.allowance) +} + +pub fn gas(chain: &str, target: &str, data: &str, value: &str) -> Result<(Option, String)> { + let response = chain_read(ChainReadRequest { + address: None, + chain: chain.to_string(), + data: Some(data.to_string()), + operation: ChainReadOperation::Gas, + owner: None, + selector: selector(data), + spender: None, + target: Some(target.to_string()), + token: None, + value: Some(value.to_string()), + })?; + let response = parse_host_value::(response)?; + Ok((response.gas_estimate, response.gas_price)) +} + +pub fn simulate( + chain: &str, + target: &str, + data: &str, + value: &str, + spender: Option<&str>, +) -> Result<()> { + host_call(HostRequest::SimulateTransaction(HostTransaction { + chain: chain.to_string(), + data: data.to_string(), + selector: selector(data), + spender: spender.map(str::to_string), + target: target.to_string(), + value: value.to_string(), + }))?; + Ok(()) +} + +pub fn diagnostic(level: &str, message: &str) -> Result<()> { + host_call(HostRequest::Diagnostic { + level: level.to_string(), + message: message.to_string(), + })?; + Ok(()) +} + +#[expect( + dead_code, + reason = "storage is part of the app SDK even when Uniswap v1 does not persist data" +)] +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) + } +} + +#[expect( + dead_code, + reason = "storage is part of the app SDK even when Uniswap v1 does not persist data" +)] +pub fn storage_set(key: &str, value: Value) -> Result<()> { + host_call(HostRequest::AppStorageSet { + key: key.to_string(), + value, + })?; + Ok(()) +} + +#[expect( + dead_code, + reason = "storage is part of the app SDK even when Uniswap v1 does not persist data" +)] +pub fn storage_remove(key: &str) -> Result<()> { + host_call(HostRequest::AppStorageRemove { + key: key.to_string(), + })?; + Ok(()) +} + +fn chain_read(request: ChainReadRequest) -> Result { + host_call(HostRequest::ChainRead(request)) +} + +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(), + }) +} + +fn is_native_token(address: &str, label: &str) -> bool { + address.eq_ignore_ascii_case("0x0000000000000000000000000000000000000000") + || label.eq_ignore_ascii_case("native") + || label.eq_ignore_ascii_case("eth") +} + +#[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/uniswap/src/lib.rs b/beam-apps/apps/uniswap/src/lib.rs index a6910b2..5792ecd 100644 --- a/beam-apps/apps/uniswap/src/lib.rs +++ b/beam-apps/apps/uniswap/src/lib.rs @@ -14,8 +14,9 @@ pub use api::{ }; pub use args::SwapArgs; pub use error::{Error, Result}; -pub use host::{ActionBinding, ActionPlan, ActionStep, PlanContext, SwapToken}; +pub use host::{ActionBinding, ActionPlan, ActionStep, GuestInvocation, PlanContext, SwapToken}; pub use plan::{SwapPlanInput, build_swap_plan}; +use serde_json::{Value, json}; #[cfg(test)] mod tests; @@ -35,6 +36,225 @@ pub extern "C" fn beam_uniswap_public_api_key_len() -> usize { } #[unsafe(no_mangle)] -pub extern "C" fn beam_app_main() { - let _ = core::hint::black_box(public_api_key()); +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_response); + 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)?; + let command = invocation + .args + .first() + .map(String::as_str) + .ok_or_else(|| Error::UnsupportedCommand { + command: "".to_string(), + })?; + match command { + "swap" => { + let plan = run_swap(invocation)?; + Ok(json!({ + "kind": "action-plan", + "plan": plan, + })) + } + other => Err(Error::UnsupportedCommand { + command: other.to_string(), + }), + } +} + +fn run_swap(invocation: GuestInvocation) -> Result { + let args = SwapArgs::parse(&invocation.args)?; + let chain = invocation.metadata.chain.clone(); + let wallet = invocation.metadata.wallet.clone(); + let recipient = host::resolve_address(args.recipient.as_deref())?; + let sell = host::token_metadata(&chain, &args.sell_token)?; + let buy = host::token_metadata(&chain, &args.buy_token)?; + let amount_raw = amount_to_raw(&args.amount, sell.decimals)?; + let min_receive_raw = args + .min_receive + .as_deref() + .map(|amount| amount_to_raw(amount, buy.decimals)) + .transpose()?; + let quote_request = QuoteRequest { + amount: amount_raw.clone(), + chain_id: invocation.metadata.chain_id, + recipient, + slippage_bps: args.slippage_bps, + token_in: sell.address.clone(), + token_out: buy.address.clone(), + wallet: wallet.clone(), + }; + let quote = parse_quote( + host::http_json( + "POST", + "https://trade-api.gateway.uniswap.org/v1/quote", + "e_payload("e_request), + )?, + "e_request, + )?; + let approval = if sell.is_native { + None + } else { + let value = host::http_json( + "POST", + "https://trade-api.gateway.uniswap.org/v1/check_approval", + &check_approval_payload("e_request), + )?; + Some(ApprovalResponse { + transaction: find_transaction(&value), + }) + }; + let allowance = approval + .as_ref() + .and_then(|approval| approval.transaction.as_ref()) + .and_then(|transaction| approval_spender(&transaction.data)) + .map(|spender| host::allowance(&chain, &sell.address, &spender)) + .transpose()?; + let swap_value = host::http_json( + "POST", + "https://trade-api.gateway.uniswap.org/v1/swap", + &swap_payload("e, &wallet), + )?; + let mut swap = SwapResponse { + transaction: find_transaction(&swap_value).ok_or_else(|| Error::InvalidUniswapResponse { + reason: "swap response missing transaction".to_string(), + })?, + raw: swap_value, + }; + if swap.transaction.gas_limit.is_none() || swap.transaction.gas_price.is_none() { + let (gas_limit, gas_price) = 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)); + } + simulate_best_effort(&chain, approval.as_ref(), &swap); + + build_swap_plan(SwapPlanInput { + allowance, + amount_raw, + args, + buy, + context: invocation.metadata.plan_context(), + expires_at: invocation.metadata.now, + min_receive_raw, + quote, + sell_balance: host::balance(&chain, &sell.address)?, + sell, + approval, + swap, + }) +} + +fn simulate_best_effort(chain: &str, approval: Option<&ApprovalResponse>, swap: &SwapResponse) { + if let Some(transaction) = approval + .and_then(|approval| approval.transaction.as_ref()) + .filter(|transaction| approval_spender(&transaction.data).is_some()) + { + let spender = approval_spender(&transaction.data); + if let Err(err) = host::simulate( + chain, + &transaction.to, + &transaction.data, + &transaction.value, + spender.as_deref(), + ) { + let _ = host::diagnostic("warn", &format!("approval simulation skipped: {err}")); + } + } + if let Err(err) = host::simulate( + chain, + &swap.transaction.to, + &swap.transaction.data, + &swap.transaction.value, + None, + ) { + let _ = host::diagnostic("warn", &format!("swap simulation skipped: {err}")); + } +} + +fn amount_to_raw(amount: &str, decimals: u8) -> Result { + let amount = amount.trim(); + let (whole, fractional) = amount.split_once('.').unwrap_or((amount, "")); + if whole.is_empty() || !whole.chars().all(|char| char.is_ascii_digit()) { + return Err(Error::InvalidInteger { + value: amount.to_string(), + }); + } + if fractional.len() > usize::from(decimals) + || !fractional.chars().all(|char| char.is_ascii_digit()) + { + return Err(Error::InvalidInteger { + value: amount.to_string(), + }); + } + let mut digits = whole.to_string(); + digits.push_str(fractional); + for _ in fractional.len()..usize::from(decimals) { + digits.push('0'); + } + let trimmed = digits.trim_start_matches('0'); + if trimmed.is_empty() { + Ok("0".to_string()) + } else { + Ok(trimmed.to_string()) + } +} + +fn error_response(error: Error) -> Value { + json!({ + "kind": "error", + "message": error.to_string(), + }) +} + +fn pack_response(value: Value) -> u64 { + let bytes = serde_json::to_vec(&value).unwrap_or_else(|err| { + format!( + r#"{{"kind":"error","message":"[beam-app-uniswap] 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 } diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json index 80268e7..770cbe4 100644 --- a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json +++ b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.1.2", "wasm": { - "sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "entrypoint": "beam_app_main" }, "catalog": { @@ -219,6 +219,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:4d53155fe87d58d1a3bcbedc8f312b12a377eb7a4ba3956575fdf225f764b45a" + "value": "sha256:7606f4bb87567420778c2688804cb3cbfdc7d8dd49d712f10bb71177fabd4a0f" } } diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json.sig b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json.sig index 4748bf1..d16074d 100644 --- a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json.sig +++ b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/module.wasm b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/module.wasm index a4fb777..1d2b3fd 100644 Binary files a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/module.wasm and b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/version.json.sig b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/version.json.sig index d6a752e..082e83b 100644 --- a/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/version.json.sig +++ b/beam-apps/fixtures/broad-wildcard/apps/uniswap/1.0.0/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } diff --git a/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json b/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json index 45413a2..a6fca62 100644 --- a/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json +++ b/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json @@ -271,7 +271,7 @@ "sensitive_args": [] } ], - "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", + "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nCommand help is exported through the app manifest and rendered by Beam without\nfetching a quote or invoking guest WASM:\n\n```bash\nbeam x uniswap swap --help\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\nToken inputs can be Beam token labels, `native`, the active chain's native\nsymbol, or EVM token addresses.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\nIf an approval is required, Beam submits it first and submits the swap only after\nthe approval is confirmed. If fresh allowance already satisfies the exact plan,\nBeam skips the approval step. Execution output reports confirmed, pending,\ndropped, or skipped transaction state; confirmed receipts include the RPC status\nvalue.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", "manifest_summary": { "format_version": 1, "min_beam_version": "0.1.2", @@ -293,6 +293,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } } diff --git a/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json.sig b/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json.sig index 486afc8..add7d00 100644 --- a/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json.sig +++ b/beam-apps/fixtures/broad-wildcard/catalog/apps/uniswap.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } diff --git a/beam-apps/fixtures/broad-wildcard/index.json b/beam-apps/fixtures/broad-wildcard/index.json index 4ace360..872607e 100644 --- a/beam-apps/fixtures/broad-wildcard/index.json +++ b/beam-apps/fixtures/broad-wildcard/index.json @@ -12,13 +12,13 @@ "version": "1.0.0", "min_beam_version": "0.1.2", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/manifest.json", - "manifest_sha256": "sha256:9b1e84f51ef0e5afd88a79aaaaa9ebf24b8d15a0a321c0ffa93c6da0ba5c8ede", + "manifest_sha256": "sha256:679a551bf48519a586975b25385e310519b077853383a4b3f7c2ff7b66ca3470", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/module.wasm", - "module_sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "module_sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } } ] @@ -27,6 +27,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } } diff --git a/beam-apps/fixtures/broad-wildcard/index.json.sig b/beam-apps/fixtures/broad-wildcard/index.json.sig index c10b635..9c8b5f1 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:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json index 76c5ba2..a953177 100644 --- a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json +++ b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.1.2", "wasm": { - "sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "entrypoint": "beam_app_main" }, "catalog": { @@ -290,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } } diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json.sig b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json.sig index 4748bf1..d16074d 100644 --- a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json.sig +++ b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/module.wasm b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/module.wasm index a4fb777..1d2b3fd 100644 Binary files a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/module.wasm and b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/version.json.sig b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/version.json.sig index d6a752e..082e83b 100644 --- a/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/version.json.sig +++ b/beam-apps/fixtures/invalid-digest/apps/uniswap/1.0.0/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } diff --git a/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json b/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json index 45413a2..a6fca62 100644 --- a/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json +++ b/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json @@ -271,7 +271,7 @@ "sensitive_args": [] } ], - "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", + "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nCommand help is exported through the app manifest and rendered by Beam without\nfetching a quote or invoking guest WASM:\n\n```bash\nbeam x uniswap swap --help\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\nToken inputs can be Beam token labels, `native`, the active chain's native\nsymbol, or EVM token addresses.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\nIf an approval is required, Beam submits it first and submits the swap only after\nthe approval is confirmed. If fresh allowance already satisfies the exact plan,\nBeam skips the approval step. Execution output reports confirmed, pending,\ndropped, or skipped transaction state; confirmed receipts include the RPC status\nvalue.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", "manifest_summary": { "format_version": 1, "min_beam_version": "0.1.2", @@ -293,6 +293,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } } diff --git a/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json.sig b/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json.sig index 486afc8..add7d00 100644 --- a/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json.sig +++ b/beam-apps/fixtures/invalid-digest/catalog/apps/uniswap.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } diff --git a/beam-apps/fixtures/invalid-digest/index.json b/beam-apps/fixtures/invalid-digest/index.json index 9a0b6f9..b20a5c9 100644 --- a/beam-apps/fixtures/invalid-digest/index.json +++ b/beam-apps/fixtures/invalid-digest/index.json @@ -12,13 +12,13 @@ "version": "1.0.0", "min_beam_version": "0.1.2", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/manifest.json", - "manifest_sha256": "sha256:9b1e84f51ef0e5afd88a79aaaaa9ebf24b8d15a0a321c0ffa93c6da0ba5c8ede", + "manifest_sha256": "sha256:679a551bf48519a586975b25385e310519b077853383a4b3f7c2ff7b66ca3470", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/module.wasm", "module_sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } } ] @@ -27,6 +27,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:9f831d41929e401c0873a58a80aeaae76a6353ec07da62a2b1864f969536eecf" + "value": "sha256:c71291f48e084cb773bc003d3fa7536acd0c653632547da79d32373107f89d6d" } } diff --git a/beam-apps/fixtures/invalid-digest/index.json.sig b/beam-apps/fixtures/invalid-digest/index.json.sig index c10b635..9c8b5f1 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:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json index b40b405..43856d7 100644 --- a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json +++ b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.1.2", "wasm": { - "sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "entrypoint": "beam_app_main" }, "catalog": { @@ -221,6 +221,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:a1b9a4d8928f731b0e778bdf8cb6c3075c895c9d8f1e06fff1e5cbb328a4dd51" + "value": "sha256:e8ec17a0f387c153dbc952556c9f4b320e15694de2c564fafc998098121fcba5" } } diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json.sig b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json.sig index 4748bf1..d16074d 100644 --- a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json.sig +++ b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/module.wasm b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/module.wasm index a4fb777..1d2b3fd 100644 Binary files a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/module.wasm and b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/version.json.sig b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/version.json.sig index d6a752e..082e83b 100644 --- a/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/version.json.sig +++ b/beam-apps/fixtures/malformed-permissions/apps/uniswap/1.0.0/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } diff --git a/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json b/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json index 45413a2..a6fca62 100644 --- a/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json +++ b/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json @@ -271,7 +271,7 @@ "sensitive_args": [] } ], - "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", + "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nCommand help is exported through the app manifest and rendered by Beam without\nfetching a quote or invoking guest WASM:\n\n```bash\nbeam x uniswap swap --help\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\nToken inputs can be Beam token labels, `native`, the active chain's native\nsymbol, or EVM token addresses.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\nIf an approval is required, Beam submits it first and submits the swap only after\nthe approval is confirmed. If fresh allowance already satisfies the exact plan,\nBeam skips the approval step. Execution output reports confirmed, pending,\ndropped, or skipped transaction state; confirmed receipts include the RPC status\nvalue.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", "manifest_summary": { "format_version": 1, "min_beam_version": "0.1.2", @@ -293,6 +293,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } } diff --git a/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json.sig b/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json.sig index 486afc8..add7d00 100644 --- a/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json.sig +++ b/beam-apps/fixtures/malformed-permissions/catalog/apps/uniswap.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } diff --git a/beam-apps/fixtures/malformed-permissions/index.json b/beam-apps/fixtures/malformed-permissions/index.json index 4ace360..872607e 100644 --- a/beam-apps/fixtures/malformed-permissions/index.json +++ b/beam-apps/fixtures/malformed-permissions/index.json @@ -12,13 +12,13 @@ "version": "1.0.0", "min_beam_version": "0.1.2", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/manifest.json", - "manifest_sha256": "sha256:9b1e84f51ef0e5afd88a79aaaaa9ebf24b8d15a0a321c0ffa93c6da0ba5c8ede", + "manifest_sha256": "sha256:679a551bf48519a586975b25385e310519b077853383a4b3f7c2ff7b66ca3470", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/module.wasm", - "module_sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "module_sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } } ] @@ -27,6 +27,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } } diff --git a/beam-apps/fixtures/malformed-permissions/index.json.sig b/beam-apps/fixtures/malformed-permissions/index.json.sig index c10b635..9c8b5f1 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:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json index 83060f0..d6ef0b8 100644 --- a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json +++ b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.1.2", "wasm": { - "sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "entrypoint": "beam_app_main" }, "catalog": { @@ -129,6 +129,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:bac5e146237b53395f744e1097794b73abaa94a4ec75c6096ab3404e1e738a06" + "value": "sha256:80eaa89c54f1d5eb38198f9de7c261e6446ffe700d6c9313232f076bb783c518" } } diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json.sig b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json.sig index 4748bf1..d16074d 100644 --- a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json.sig +++ b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/module.wasm b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/module.wasm index a4fb777..1d2b3fd 100644 Binary files a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/module.wasm and b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/version.json.sig b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/version.json.sig index d6a752e..082e83b 100644 --- a/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/version.json.sig +++ b/beam-apps/fixtures/missing-fields/apps/uniswap/1.0.0/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } diff --git a/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json b/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json index 45413a2..a6fca62 100644 --- a/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json +++ b/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json @@ -271,7 +271,7 @@ "sensitive_args": [] } ], - "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", + "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nCommand help is exported through the app manifest and rendered by Beam without\nfetching a quote or invoking guest WASM:\n\n```bash\nbeam x uniswap swap --help\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\nToken inputs can be Beam token labels, `native`, the active chain's native\nsymbol, or EVM token addresses.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\nIf an approval is required, Beam submits it first and submits the swap only after\nthe approval is confirmed. If fresh allowance already satisfies the exact plan,\nBeam skips the approval step. Execution output reports confirmed, pending,\ndropped, or skipped transaction state; confirmed receipts include the RPC status\nvalue.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", "manifest_summary": { "format_version": 1, "min_beam_version": "0.1.2", @@ -293,6 +293,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } } diff --git a/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json.sig b/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json.sig index 486afc8..add7d00 100644 --- a/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json.sig +++ b/beam-apps/fixtures/missing-fields/catalog/apps/uniswap.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } diff --git a/beam-apps/fixtures/missing-fields/index.json b/beam-apps/fixtures/missing-fields/index.json index 4ace360..872607e 100644 --- a/beam-apps/fixtures/missing-fields/index.json +++ b/beam-apps/fixtures/missing-fields/index.json @@ -12,13 +12,13 @@ "version": "1.0.0", "min_beam_version": "0.1.2", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/manifest.json", - "manifest_sha256": "sha256:9b1e84f51ef0e5afd88a79aaaaa9ebf24b8d15a0a321c0ffa93c6da0ba5c8ede", + "manifest_sha256": "sha256:679a551bf48519a586975b25385e310519b077853383a4b3f7c2ff7b66ca3470", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/module.wasm", - "module_sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "module_sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } } ] @@ -27,6 +27,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } } diff --git a/beam-apps/fixtures/missing-fields/index.json.sig b/beam-apps/fixtures/missing-fields/index.json.sig index c10b635..9c8b5f1 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:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json index 9f1acb7..a8bf809 100644 --- a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json +++ b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "999.0.0", "wasm": { - "sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "entrypoint": "beam_app_main" }, "catalog": { @@ -290,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:fe22b97b09a983b2f31844a57dce22187c9fbf08ca4a175d944c10bf4e9a2beb" + "value": "sha256:52aa83b6ee6df8f59c6abbfef5593fcb4f05c0b6d2a9d7468de5c0428ecc0d15" } } diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json.sig b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json.sig index 4748bf1..d16074d 100644 --- a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json.sig +++ b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/module.wasm b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/module.wasm index a4fb777..1d2b3fd 100644 Binary files a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/module.wasm and b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/version.json.sig b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/version.json.sig index d6a752e..082e83b 100644 --- a/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/version.json.sig +++ b/beam-apps/fixtures/unsupported-beam/apps/uniswap/1.0.0/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } diff --git a/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json b/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json index 45413a2..a6fca62 100644 --- a/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json +++ b/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json @@ -271,7 +271,7 @@ "sensitive_args": [] } ], - "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", + "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nCommand help is exported through the app manifest and rendered by Beam without\nfetching a quote or invoking guest WASM:\n\n```bash\nbeam x uniswap swap --help\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\nToken inputs can be Beam token labels, `native`, the active chain's native\nsymbol, or EVM token addresses.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\nIf an approval is required, Beam submits it first and submits the swap only after\nthe approval is confirmed. If fresh allowance already satisfies the exact plan,\nBeam skips the approval step. Execution output reports confirmed, pending,\ndropped, or skipped transaction state; confirmed receipts include the RPC status\nvalue.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", "manifest_summary": { "format_version": 1, "min_beam_version": "0.1.2", @@ -293,6 +293,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } } diff --git a/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json.sig b/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json.sig index 486afc8..add7d00 100644 --- a/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json.sig +++ b/beam-apps/fixtures/unsupported-beam/catalog/apps/uniswap.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } diff --git a/beam-apps/fixtures/unsupported-beam/index.json b/beam-apps/fixtures/unsupported-beam/index.json index 4ace360..872607e 100644 --- a/beam-apps/fixtures/unsupported-beam/index.json +++ b/beam-apps/fixtures/unsupported-beam/index.json @@ -12,13 +12,13 @@ "version": "1.0.0", "min_beam_version": "0.1.2", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/manifest.json", - "manifest_sha256": "sha256:9b1e84f51ef0e5afd88a79aaaaa9ebf24b8d15a0a321c0ffa93c6da0ba5c8ede", + "manifest_sha256": "sha256:679a551bf48519a586975b25385e310519b077853383a4b3f7c2ff7b66ca3470", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/module.wasm", - "module_sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "module_sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } } ] @@ -27,6 +27,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } } diff --git a/beam-apps/fixtures/unsupported-beam/index.json.sig b/beam-apps/fixtures/unsupported-beam/index.json.sig index c10b635..9c8b5f1 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:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json index 76c5ba2..a953177 100644 --- a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json +++ b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json @@ -7,7 +7,7 @@ "description": "Prepare public Uniswap swaps through Beam-mediated HTTP, chain, approval, and transaction planning.", "min_beam_version": "0.1.2", "wasm": { - "sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "entrypoint": "beam_app_main" }, "catalog": { @@ -290,6 +290,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } } diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json.sig b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json.sig index 4748bf1..d16074d 100644 --- a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json.sig +++ b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/manifest.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:6fa02552dc669810b58d6fe9a55cc36546221c3b25cf9340ab6ff74388f372fb" + "value": "sha256:2f47a3e064fdc3572cc68eb52929147275f5e57fc958e82e59e8c2a5f21ab57e" } diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/module.wasm b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/module.wasm index a4fb777..1d2b3fd 100644 Binary files a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/module.wasm and b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/module.wasm differ diff --git a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/version.json.sig b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/version.json.sig index d6a752e..082e83b 100644 --- a/beam-apps/fixtures/valid/apps/uniswap/1.0.0/version.json.sig +++ b/beam-apps/fixtures/valid/apps/uniswap/1.0.0/version.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } diff --git a/beam-apps/fixtures/valid/catalog/apps/uniswap.json b/beam-apps/fixtures/valid/catalog/apps/uniswap.json index 45413a2..a6fca62 100644 --- a/beam-apps/fixtures/valid/catalog/apps/uniswap.json +++ b/beam-apps/fixtures/valid/catalog/apps/uniswap.json @@ -271,7 +271,7 @@ "sensitive_args": [] } ], - "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", + "readme_markdown": "# Uniswap App\n\nThe Uniswap app turns a swap request into a Beam-approved action plan. It asks\nthe Uniswap Trading API for a quote, checks your current ERC-20 allowance,\nprepares an exact approval only when one is needed, builds the swap transaction,\nand hands the whole plan to Beam for approval and execution.\n\nBeam owns your wallet boundary. The app never receives private keys, cannot sign\ntransactions, and cannot send a transaction on its own.\n\n## Install\n\n```bash\nbeam apps install uniswap\n```\n\nBeam shows the publisher, version, supported chains, network access, wallet\ncapabilities, and storage permissions before activating the app. To inspect the\nsame permission summary without installing:\n\n```bash\nbeam apps install uniswap --dry-run\n```\n\n## Swap\n\n```bash\nbeam x uniswap swap [options]\n```\n\nCommand help is exported through the app manifest and rendered by Beam without\nfetching a quote or invoking guest WASM:\n\n```bash\nbeam x uniswap swap --help\n```\n\nExample:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice\n```\n\nBeam shows the quote, any required approval, and the swap as a single plan. You\napprove the final plan before Beam signs or submits anything.\n\n## Options\n\n- `--min-receive ` sets the minimum acceptable output amount.\n- `--max-gas ` rejects the plan if estimated gas exceeds the limit.\n- `--slippage-bps ` sets max slippage in basis points.\n- `--recipient ` sends output to another wallet, ENS name, or\n EVM address.\n- `--deadline-seconds ` sets the quote and transaction deadline window.\n- `--unlimited-approval` asks Beam to request an unlimited ERC-20 approval\n instead of the default exact approval.\n\nToken inputs can be Beam token labels, `native`, the active chain's native\nsymbol, or EVM token addresses.\n\n## How a Swap Works\n\n1. The app fetches a quote through Beam-mediated HTTPS access.\n2. Beam reads your token balance and allowance through its chain APIs.\n3. If allowance is short, Beam adds an exact ERC-20 approval step.\n4. The app prepares the swap transaction.\n5. Beam renders the typed plan, asks for approval, signs, submits, and tracks the\n receipt.\n\nIf an approval is required, Beam submits it first and submits the swap only after\nthe approval is confirmed. If fresh allowance already satisfies the exact plan,\nBeam skips the approval step. Execution output reports confirmed, pending,\ndropped, or skipped transaction state; confirmed receipts include the RPC status\nvalue.\n\n## Agents\n\nAgents and other non-interactive callers should prepare a continuation, inspect\nit, then explicitly approve and execute it:\n\n```bash\nbeam x uniswap swap USDC ETH 100 --chain base --from alice --prepare --format json\nbeam apps approvals show \nbeam apps approvals approve --execute\n```\n\n`--no-prompt` fails closed for wallet-affecting swaps unless the command is\npreparing a continuation or executing an already-approved continuation.\n\n## Permissions\n\nThe app requests HTTPS access to the Uniswap Trading API, read/simulate/send\naccess on supported public EVM chains, ERC-20 approval planning, wallet balance\nreads, transaction proposals, and app-local storage. It does not request Beam\nprivacy capabilities in v1.\n\nSupported chains are Ethereum, Base, Arbitrum, Polygon, BNB, and Sepolia.\n\nApprovals default to the exact amount required. Unlimited approvals require the\nexplicit `--unlimited-approval` flag and are shown in the Beam approval prompt.\n", "manifest_summary": { "format_version": 1, "min_beam_version": "0.1.2", @@ -293,6 +293,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } } diff --git a/beam-apps/fixtures/valid/catalog/apps/uniswap.json.sig b/beam-apps/fixtures/valid/catalog/apps/uniswap.json.sig index 486afc8..add7d00 100644 --- a/beam-apps/fixtures/valid/catalog/apps/uniswap.json.sig +++ b/beam-apps/fixtures/valid/catalog/apps/uniswap.json.sig @@ -1,5 +1,5 @@ { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:751da26b235d861af214324b036ba5ee5c20062e80e66f6104cd78ba7b2672de" + "value": "sha256:cf1ff80f294be2c782b248b643bf45ad2aa8d1c45d4fd48ef3e160e601037b42" } diff --git a/beam-apps/fixtures/valid/index.json b/beam-apps/fixtures/valid/index.json index 4ace360..872607e 100644 --- a/beam-apps/fixtures/valid/index.json +++ b/beam-apps/fixtures/valid/index.json @@ -12,13 +12,13 @@ "version": "1.0.0", "min_beam_version": "0.1.2", "manifest_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/manifest.json", - "manifest_sha256": "sha256:9b1e84f51ef0e5afd88a79aaaaa9ebf24b8d15a0a321c0ffa93c6da0ba5c8ede", + "manifest_sha256": "sha256:679a551bf48519a586975b25385e310519b077853383a4b3f7c2ff7b66ca3470", "module_url": "https://registry.beam.payy.network/apps/uniswap/1.0.0/module.wasm", - "module_sha256": "sha256:b155ff4bbbab293085ee393fc12cd9ddcda0e531694521909cd8b5c433ecb927", + "module_sha256": "sha256:15dadd2f783c783594c5c586f494f7b31be6dc89e5efbab1c479cf317b15ad0f", "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:5b8c5aa5f89bf386efbbcdb3a8c670e24bf7a583c9ed0f9c327dedcdeb5a87f0" + "value": "sha256:d66c14c15fb72b7d083a8a61984e2356a130b521fb70e664edefcfabd08ed8dd" } } ] @@ -27,6 +27,6 @@ "signature": { "algorithm": "sha256-dev", "key_id": "payy-dev-2026-05", - "value": "sha256:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } } diff --git a/beam-apps/fixtures/valid/index.json.sig b/beam-apps/fixtures/valid/index.json.sig index c10b635..9c8b5f1 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:de6a24df0daabff15742faa1754c8c254a1d4157a55107766cee0a873e914411" + "value": "sha256:40a78ac42754c32d9e50aab13e98882852a73f14155a648919d1fdae928e78a8" } diff --git a/pkg/beam-cli/README.md b/pkg/beam-cli/README.md index ffcca9a..4ba2dbf 100644 --- a/pkg/beam-cli/README.md +++ b/pkg/beam-cli/README.md @@ -250,14 +250,17 @@ Run app commands with the short `x` alias or the explicit lifecycle form: ```bash 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 ``` Product app business logic lives outside Beam CLI in `beam-apps/apps/`. Beam CLI owns the generic registry, cache, WASM validation, permission checks, -approval records, and execution of approved action plans. The Uniswap app is -built into the registry as WASM, but `beam x uniswap swap ...` remains behind the -generic guest host-ABI invocation milestone; Beam CLI no longer contains a -Uniswap-specific built-in planner. +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. 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 @@ -285,9 +288,18 @@ beam apps approvals show beam apps approvals approve --execute ``` -`--no-prompt` fails closed for wallet-affecting app commands unless the command is -preparing a continuation. Removing an app keeps app-local data by default; pass -`--purge-data` to delete `~/.beam/apps/data/` as well. +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 +`--unlimited-approval`. Approvals default to the exact amount required and the +swap is only sent after an approval is confirmed or skipped because fresh +allowance is already sufficient. Execution output reports confirmed, pending, +dropped, or skipped transaction state as Beam receives it from the active RPC +path; confirmed receipts include the reported transaction status. + +`--no-prompt` fails closed for wallet-affecting app commands unless the command +is preparing a continuation. Removing an app keeps app-local data by default; +pass `--purge-data` to delete `~/.beam/apps/data/` as well. ## Privacy diff --git a/pkg/beam-cli/src/apps/approvals.rs b/pkg/beam-cli/src/apps/approvals.rs index e0f9a85..d453722 100644 --- a/pkg/beam-cli/src/apps/approvals.rs +++ b/pkg/beam-cli/src/apps/approvals.rs @@ -1,3 +1,4 @@ +// lint-long-file-override allow-max-lines=300 use std::path::Path; use contextful::ResultContextExt; @@ -158,7 +159,8 @@ pub fn ensure_approval_executable(record: &ApprovalRecord) -> Result<()> { pub fn ensure_approval_integrity(record: &ApprovalRecord) -> Result<()> { ensure_approval_active(record)?; - ensure_approval_plan_hash(record) + ensure_approval_plan_hash(record)?; + ensure_uniswap_swap_bindings(record) } pub fn ensure_approval_active(record: &ApprovalRecord) -> Result<()> { @@ -185,3 +187,49 @@ pub fn plan_hash(plan: &ActionPlan) -> Result { let bytes = serde_json::to_vec(plan).context("encode beam app action plan")?; Ok(format!("sha256:{}", hex::encode(Sha256::digest(bytes)))) } + +fn ensure_uniswap_swap_bindings(record: &ApprovalRecord) -> Result<()> { + if record.plan.app_id != "uniswap" || !record.plan.command.starts_with("swap ") { + return Ok(()); + } + + for key in [ + "quote_id", + "quote_expires_at", + "route_hash", + "swap_calldata_hash", + "router", + "sell_token", + "buy_token", + "amount_in", + "amount_out", + ] { + if binding(record, key).is_none() { + return Err(Error::InvalidGuestOutput { + reason: format!("uniswap approval missing binding {key}"), + }); + } + } + + let quote_expires_at = binding(record, "quote_expires_at") + .and_then(|value| value.parse::().ok()) + .ok_or_else(|| Error::InvalidGuestOutput { + reason: "uniswap approval has invalid quote_expires_at binding".to_string(), + })?; + if quote_expires_at < now() { + return Err(Error::ApprovalExpired { + approval_id: record.id.clone(), + }); + } + + Ok(()) +} + +fn binding<'a>(record: &'a ApprovalRecord, key: &str) -> Option<&'a str> { + record + .plan + .bindings + .iter() + .find(|binding| binding.key == key) + .map(|binding| binding.value.as_str()) +} diff --git a/pkg/beam-cli/src/apps/error.rs b/pkg/beam-cli/src/apps/error.rs index ea7ff2f..cb5b242 100644 --- a/pkg/beam-cli/src/apps/error.rs +++ b/pkg/beam-cli/src/apps/error.rs @@ -58,6 +58,15 @@ pub enum Error { #[error("[beam-cli/apps] app module is not a wasm module: {app}")] InvalidWasmModule { app: String }, + #[error("[beam-cli/apps] app wasm missing export `{export}` for {app}")] + MissingWasmExport { app: String, export: String }, + + #[error("[beam-cli/apps] app command failed: {message}")] + GuestCommandFailed { message: String }, + + #[error("[beam-cli/apps] app guest output is invalid: {reason}")] + InvalidGuestOutput { reason: String }, + #[error("[beam-cli/apps] app requested blocked contract target: {target}")] ContractPermissionDenied { target: String }, diff --git a/pkg/beam-cli/src/apps/host.rs b/pkg/beam-cli/src/apps/host.rs index 8401cf4..9866983 100644 --- a/pkg/beam-cli/src/apps/host.rs +++ b/pkg/beam-cli/src/apps/host.rs @@ -1,9 +1,5 @@ -// lint-long-file-override allow-max-lines=550 -#![expect( - dead_code, - reason = "declares host ABI surface before wasm guest bindings call every API" -)] -use std::{net::IpAddr, time::Duration}; +// lint-long-file-override allow-max-lines=700 +use std::{fs, net::IpAddr, path::PathBuf, time::Duration}; use contextful::ResultContextExt; use contracts::{Address, U256}; @@ -31,6 +27,19 @@ const MAX_REDIRECTS: usize = 5; const MAX_REQUEST_BYTES: usize = 64 * 1024; const MAX_RESPONSE_BYTES: usize = 1024 * 1024; +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct HostMetadata { + pub app_id: String, + pub app_version: String, + pub chain: String, + pub chain_id: u64, + pub host_api_version: u32, + pub manifest_sha256: String, + pub now: u64, + pub wallet: String, + pub wasm_sha256: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum HostRequest { @@ -43,6 +52,37 @@ pub enum HostRequest { SimulateTransaction(HostTransaction), SubmitTransaction(HostTransaction), PollReceipt { tx_hash: String }, + ResolveAddress { value: Option }, + AppStorageGet { key: String }, + AppStorageSet { key: String, value: Value }, + AppStorageRemove { key: String }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct HostCallResponse { + pub ok: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl HostCallResponse { + pub fn ok(value: Value) -> Self { + Self { + ok: true, + value: Some(value), + error: None, + } + } + + pub fn error(error: String) -> Self { + Self { + ok: false, + value: None, + error: Some(error), + } + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -110,6 +150,116 @@ pub struct HostTransaction { pub spender: Option, } +pub async fn handle_host_request( + app: &BeamApp, + permissions: &AppPermissions, + metadata: &HostMetadata, + request: HostRequest, + structured_output: &mut Option, + diagnostics: &mut Vec, +) -> Result { + match request { + HostRequest::AppMetadata => Ok(json!(metadata)), + HostRequest::Args { args } => Ok(json!({ "args": args })), + HostRequest::StructuredOutput { value } => { + *structured_output = Some(value.clone()); + Ok(json!({ "accepted": true })) + } + HostRequest::Diagnostic { level, message } => { + diagnostics.push(json!({ + "level": level, + "message": message, + })); + Ok(json!({ "accepted": true })) + } + HostRequest::HttpFetch(request) => Ok(json!(fetch_http(permissions, request).await?)), + HostRequest::ChainRead(request) => chain_read(app, permissions, request).await, + HostRequest::SimulateTransaction(transaction) => { + let (_, client) = app + .active_chain_client() + .await + .context("connect app simulation chain client")?; + let from = app + .active_address() + .await + .context("resolve app simulation wallet")?; + simulate_transaction(&client, from, permissions, &transaction).await?; + Ok(json!({ "ok": true })) + } + HostRequest::SubmitTransaction(_) => Err(Error::InvalidHostRequest { + reason: "transaction submission is only available during approved plan execution" + .to_string(), + }), + HostRequest::PollReceipt { .. } => Err(Error::InvalidHostRequest { + reason: "receipt polling is only available during approved plan execution".to_string(), + }), + HostRequest::ResolveAddress { value } => { + let address = match value.as_deref() { + Some(value) => app + .resolve_wallet_or_address(value) + .await + .context("resolve beam app requested address")?, + None => app + .active_address() + .await + .context("resolve beam app wallet")?, + }; + 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 })) + } + 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 })) + } + 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 })) + } + } +} + +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}"), + }); + } + + Ok(app + .paths + .root + .join("apps") + .join("data") + .join(app_id) + .join(key)) +} + pub fn ensure_http_allowed(permissions: &AppPermissions, url: &str) -> Result { let url = Url::parse(url).map_err(|_| Error::InvalidPermissionUrl { value: url.to_string(), diff --git a/pkg/beam-cli/src/apps/runtime.rs b/pkg/beam-cli/src/apps/runtime.rs index 16d3771..8bd9aec 100644 --- a/pkg/beam-cli/src/apps/runtime.rs +++ b/pkg/beam-cli/src/apps/runtime.rs @@ -1,11 +1,30 @@ +// lint-long-file-override allow-max-lines=300 use std::path::Path; use contextful::ResultContextExt; -use wasmi::{Engine, Linker, Module, Store}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use wasmi::{Config, Engine, Linker, Module, Store}; -use crate::apps::{Error, Result}; +use crate::{ + apps::{ + Error, Result, + host::HostMetadata, + model::{ActionPlan, AppManifest, InstalledApp}, + store::now, + }, + output::{CommandOutput, OutputMode}, + runtime::BeamApp, +}; + +mod guest; + +use guest::{HostState, guest_alloc, register_host_imports, typed_func, unpack_ptr_len}; const WASM_MAGIC: &[u8; 4] = b"\0asm"; +const HOST_API_VERSION: u32 = 1; +const MAX_GUEST_RESPONSE_BYTES: usize = 2 * 1024 * 1024; +const WASM_FUEL: u64 = 30_000_000; pub fn validate_wasm_module(app_id: &str, entrypoint: &str, path: &Path) -> Result<()> { let bytes = std::fs::read(path).context("read beam app wasm module")?; @@ -14,32 +33,192 @@ pub fn validate_wasm_module(app_id: &str, entrypoint: &str, path: &Path) -> Resu app: app_id.to_string(), }); } - AppRuntime::default().instantiate(app_id, entrypoint, &bytes)?; + AppRuntime::default().instantiate_for_validation(app_id, entrypoint, &bytes)?; Ok(()) } -#[derive(Default)] pub struct AppRuntime { engine: Engine, } +impl Default for AppRuntime { + fn default() -> Self { + let mut config = Config::default(); + config.consume_fuel(true); + Self { + engine: Engine::new(&config), + } + } +} + impl AppRuntime { - fn instantiate(&self, app_id: &str, entrypoint: &str, bytes: &[u8]) -> Result<()> { + fn instantiate_for_validation( + &self, + app_id: &str, + entrypoint: &str, + bytes: &[u8], + ) -> Result<()> { let module = Module::new(&self.engine, bytes).context("compile beam app wasm module")?; - let mut store = Store::new(&self.engine, HostState); - let linker = >::new(&self.engine); + let mut store = Store::new(&self.engine, HostState::validation()); + store + .set_fuel(WASM_FUEL) + .context("set beam app wasm fuel")?; + store.limiter(|state| &mut state.limits); + let mut linker = >::new(&self.engine); + register_host_imports(&mut linker)?; let instance = linker .instantiate_and_start(&mut store, &module) .context("instantiate beam app wasm module")?; if instance.get_func(&store, entrypoint).is_none() { - return Err(Error::InvalidHostRequest { - reason: format!("{app_id} wasm missing entrypoint {entrypoint}"), + return Err(Error::MissingWasmExport { + app: app_id.to_string(), + export: entrypoint.to_string(), }); } Ok(()) } + + pub async fn run_command( + &self, + app: &BeamApp, + manifest: &AppManifest, + installed: &InstalledApp, + module_path: &Path, + args: &[String], + ) -> Result { + let bytes = std::fs::read(module_path).context("read beam app wasm module")?; + let module = Module::new(&self.engine, &bytes).context("compile beam app wasm module")?; + let metadata = self.metadata(app, manifest, installed).await?; + let invocation = GuestInvocation { + args: args.to_vec(), + host_api_version: HOST_API_VERSION, + metadata: metadata.clone(), + output_mode: output_mode_label(app.output_mode).to_string(), + }; + let mut store = Store::new( + &self.engine, + HostState::new(app.clone(), manifest.permissions.clone(), metadata), + ); + store + .set_fuel(WASM_FUEL) + .context("set beam app wasm fuel")?; + store.limiter(|state| &mut state.limits); + let mut linker = >::new(&self.engine); + register_host_imports(&mut linker)?; + let instance = linker + .instantiate_and_start(&mut store, &module) + .context("instantiate beam app wasm module")?; + + let memory = + instance + .get_memory(&store, "memory") + .ok_or_else(|| Error::MissingWasmExport { + app: manifest.id.clone(), + export: "memory".to_string(), + })?; + let alloc = typed_func::(&store, &instance, "beam_alloc", &manifest.id)?; + let free = typed_func::<(i32, i32), ()>(&store, &instance, "beam_free", &manifest.id)?; + let main = typed_func::<(i32, i32), i64>( + &store, + &instance, + &manifest.wasm.entrypoint, + &manifest.id, + )?; + let input = serde_json::to_vec(&invocation).context("serialize beam app invocation")?; + let input_ptr = guest_alloc(&mut store, &alloc, input.len())?; + memory + .write(&mut store, input_ptr, &input) + .context("write beam app invocation")?; + let packed = main + .call(&mut store, (input_ptr as i32, input.len() as i32)) + .context("call beam app command")?; + free.call(&mut store, (input_ptr as i32, input.len() as i32)) + .context("free beam app invocation")?; + let (output_ptr, output_len) = unpack_ptr_len(packed)?; + if output_len > MAX_GUEST_RESPONSE_BYTES { + return Err(Error::InvalidGuestOutput { + reason: format!("guest response too large: {output_len} bytes"), + }); + } + let mut output = vec![0_u8; output_len]; + memory + .read(&store, output_ptr, &mut output) + .context("read beam app command output")?; + free.call(&mut store, (output_ptr as i32, output_len as i32)) + .context("free beam app command output")?; + + let response = + serde_json::from_slice::(&output).context("decode beam app output")?; + match response { + GuestResponse::ActionPlan { plan } => Ok(GuestCommandResult::ActionPlan(*plan)), + GuestResponse::Output { value } => { + let text = value + .get("message") + .and_then(Value::as_str) + .unwrap_or("App command completed") + .to_string(); + Ok(GuestCommandResult::Output(CommandOutput::new(text, value))) + } + GuestResponse::Error { message } => Err(Error::GuestCommandFailed { message }), + } + } + + async fn metadata( + &self, + app: &BeamApp, + manifest: &AppManifest, + installed: &InstalledApp, + ) -> Result { + let chain = app.active_chain().await.context("resolve beam app chain")?; + let wallet = app + .active_address() + .await + .context("resolve beam app wallet")?; + Ok(HostMetadata { + app_id: manifest.id.clone(), + app_version: manifest.version.clone(), + chain: chain.entry.key, + chain_id: chain.entry.chain_id, + host_api_version: HOST_API_VERSION, + manifest_sha256: installed.manifest_sha256.clone(), + now: now(), + wallet: format!("{wallet:#x}"), + wasm_sha256: installed.module_sha256.clone(), + }) + } +} + +#[derive(Debug)] +pub enum GuestCommandResult { + ActionPlan(ActionPlan), + Output(CommandOutput), } -struct HostState; +#[derive(Serialize)] +struct GuestInvocation { + args: Vec, + host_api_version: u32, + metadata: HostMetadata, + output_mode: String, +} + +#[derive(Deserialize)] +#[serde(tag = "kind", rename_all = "kebab-case")] +enum GuestResponse { + ActionPlan { plan: Box }, + Output { value: Value }, + Error { message: String }, +} + +fn output_mode_label(mode: OutputMode) -> &'static str { + match mode { + OutputMode::Default => "default", + OutputMode::Json => "json", + OutputMode::Yaml => "yaml", + OutputMode::Markdown => "markdown", + OutputMode::Compact => "compact", + OutputMode::Quiet => "quiet", + } +} diff --git a/pkg/beam-cli/src/apps/runtime/guest.rs b/pkg/beam-cli/src/apps/runtime/guest.rs new file mode 100644 index 0000000..40ad03d --- /dev/null +++ b/pkg/beam-cli/src/apps/runtime/guest.rs @@ -0,0 +1,225 @@ +// lint-long-file-override allow-max-lines=300 +use contextful::ResultContextExt; +use serde_json::Value; +use tokio::runtime::Handle; +use wasmi::{ + Caller, Extern, Instance, Linker, Memory, Store, StoreLimits, StoreLimitsBuilder, TypedFunc, + WasmParams, WasmResults, +}; + +use crate::{ + apps::{ + Error, Result, + host::{HostCallResponse, HostMetadata, HostRequest, handle_host_request}, + model::AppPermissions, + }, + runtime::BeamApp, +}; + +const MAX_WASM_MEMORY_BYTES: usize = 64 * 1024 * 1024; + +pub(super) struct HostState { + app: Option, + diagnostics: Vec, + pub(super) limits: StoreLimits, + metadata: Option, + permissions: Option, + structured_output: Option, +} + +impl HostState { + pub(super) fn new(app: BeamApp, permissions: AppPermissions, metadata: HostMetadata) -> Self { + Self { + app: Some(app), + diagnostics: Vec::new(), + limits: StoreLimitsBuilder::new() + .memory_size(MAX_WASM_MEMORY_BYTES) + .build(), + metadata: Some(metadata), + permissions: Some(permissions), + structured_output: None, + } + } + + pub(super) fn validation() -> Self { + Self { + app: None, + diagnostics: Vec::new(), + limits: StoreLimitsBuilder::new() + .memory_size(MAX_WASM_MEMORY_BYTES) + .build(), + metadata: None, + permissions: None, + structured_output: None, + } + } +} + +pub(super) fn register_host_imports(linker: &mut Linker) -> Result<()> { + linker + .func_wrap( + "env", + "beam_host_call", + |caller: Caller, + request_ptr: i32, + request_len: i32, + response_ptr: i32, + response_capacity: i32| + -> i32 { + host_call( + caller, + request_ptr, + request_len, + response_ptr, + response_capacity, + ) + .unwrap_or_else(|error| { + serde_json::to_vec(&HostCallResponse::error(error.to_string())) + .ok() + .and_then(|bytes| i32::try_from(bytes.len()).ok()) + .map(|len| -len) + .unwrap_or(i32::MIN) + }) + }, + ) + .context("register beam app host call")?; + Ok(()) +} + +pub(super) fn typed_func( + store: &Store, + instance: &Instance, + name: &str, + app_id: &str, +) -> Result> +where + Params: WasmParams, + Results: WasmResults, +{ + let func = instance + .get_func(store, name) + .ok_or_else(|| Error::MissingWasmExport { + app: app_id.to_string(), + export: name.to_string(), + })?; + Ok(func.typed(store).context("type beam app wasm export")?) +} + +pub(super) fn guest_alloc( + store: &mut Store, + alloc: &TypedFunc, + len: usize, +) -> Result { + let len = i32::try_from(len).map_err(|_| Error::InvalidHostRequest { + reason: format!("guest allocation too large: {len} bytes"), + })?; + let ptr = alloc + .call(store, len) + .context("allocate beam app guest memory")?; + checked_ptr(ptr, "guest allocation pointer") +} + +pub(super) fn unpack_ptr_len(value: i64) -> Result<(usize, usize)> { + let value = u64::try_from(value).map_err(|_| Error::InvalidGuestOutput { + reason: format!("negative guest pointer/length result: {value}"), + })?; + let ptr = (value >> 32) as u32; + let len = (value & 0xffff_ffff) as u32; + Ok((ptr as usize, len as usize)) +} + +fn host_call( + mut caller: Caller, + request_ptr: i32, + request_len: i32, + response_ptr: i32, + response_capacity: i32, +) -> Result { + let request_len = checked_len(request_len, "request length")?; + let response_capacity = checked_len(response_capacity, "response capacity")?; + let memory = caller_memory(&caller)?; + let mut request_bytes = vec![0_u8; request_len]; + memory + .read( + &caller, + checked_ptr(request_ptr, "request pointer")?, + &mut request_bytes, + ) + .context("read beam app host request")?; + let request = serde_json::from_slice::(&request_bytes) + .context("decode beam app host request")?; + let app = caller + .data() + .app + .clone() + .ok_or_else(|| Error::InvalidHostRequest { + reason: "host call is unavailable during validation".to_string(), + })?; + let permissions = + caller + .data() + .permissions + .clone() + .ok_or_else(|| Error::InvalidHostRequest { + reason: "host permissions missing".to_string(), + })?; + let metadata = caller + .data() + .metadata + .clone() + .ok_or_else(|| Error::InvalidHostRequest { + reason: "host metadata missing".to_string(), + })?; + let mut structured_output = caller.data().structured_output.clone(); + let mut diagnostics = caller.data().diagnostics.clone(); + let result = tokio::task::block_in_place(|| { + Handle::current().block_on(handle_host_request( + &app, + &permissions, + &metadata, + request, + &mut structured_output, + &mut diagnostics, + )) + }); + caller.data_mut().structured_output = structured_output; + caller.data_mut().diagnostics = diagnostics; + let response = match result { + Ok(value) => HostCallResponse::ok(value), + Err(error) => HostCallResponse::error(error.to_string()), + }; + let response = serde_json::to_vec(&response).context("serialize beam app host response")?; + if response.len() > response_capacity { + return Ok(-i32::try_from(response.len()).unwrap_or(i32::MAX)); + } + memory + .write( + &mut caller, + checked_ptr(response_ptr, "response pointer")?, + &response, + ) + .context("write beam app host response")?; + Ok(i32::try_from(response.len()).unwrap_or(i32::MAX)) +} + +fn caller_memory(caller: &Caller) -> Result { + match caller.get_export("memory") { + Some(Extern::Memory(memory)) => Ok(memory), + _ => Err(Error::MissingWasmExport { + app: "guest".to_string(), + export: "memory".to_string(), + }), + } +} + +fn checked_ptr(value: i32, field: &str) -> Result { + usize::try_from(value).map_err(|_| Error::InvalidHostRequest { + reason: format!("invalid {field}: {value}"), + }) +} + +fn checked_len(value: i32, field: &str) -> Result { + usize::try_from(value).map_err(|_| Error::InvalidHostRequest { + reason: format!("invalid {field}: {value}"), + }) +} diff --git a/pkg/beam-cli/src/commands/apps/execution.rs b/pkg/beam-cli/src/commands/apps/execution.rs index 30adb2d..6a07530 100644 --- a/pkg/beam-cli/src/commands/apps/execution.rs +++ b/pkg/beam-cli/src/commands/apps/execution.rs @@ -67,7 +67,11 @@ pub async fn execute_plan(app: &BeamApp, plan: &ActionPlan) -> Result Result bool { + if step.kind != "erc20-approval" { + return true; + } + matches!( + execution, + TransactionExecution::Confirmed(outcome) if outcome.status.unwrap_or(1) != 0 + ) +} + fn render_simulated_execution(plan: &ActionPlan) -> CommandOutput { CommandOutput::new( format!("Executed app action: {}", plan.command), diff --git a/pkg/beam-cli/src/commands/apps/mod.rs b/pkg/beam-cli/src/commands/apps/mod.rs index 873412f..60a2ddf 100644 --- a/pkg/beam-cli/src/commands/apps/mod.rs +++ b/pkg/beam-cli/src/commands/apps/mod.rs @@ -18,7 +18,7 @@ use crate::{ ensure_manifest_matches, fetch_index, fetch_manifest, fetch_module, registry_url_from_env, select_app, select_version, }, - runtime::validate_wasm_module, + runtime::{AppRuntime, GuestCommandResult, validate_wasm_module}, store::AppCache, }, cli::{AppApprovalAction, AppInstallArgs, AppRemoveArgs, AppRunArgs, AppsAction}, @@ -29,12 +29,12 @@ use crate::{ }; use execution::execute_plan; -use plans::{plan_for_command, validate_plan_permissions}; +use plans::{validate_guest_plan, validate_plan_permissions}; use prompt::approve_interactively; use render::{ - approval_json, manifest_json, permissions_json, render_app_help, render_approval, - render_approval_created, render_execution, render_install_summary, render_manifest_info, - render_permission_diff, render_permissions, + app_command_json, approval_json, manifest_json, permissions_json, render_app_command_help, + render_app_help, render_approval, render_approval_created, render_install_summary, + render_manifest_info, render_permission_diff, render_permissions, }; pub async fn run(app: &BeamApp, action: AppsAction) -> Result<()> { @@ -68,12 +68,40 @@ pub async fn run_app(app: &BeamApp, args: AppRunArgs) -> Result<()> { .first() .cloned() .unwrap_or_else(|| "help".to_string()); - if command == "help" || args.args.iter().any(|arg| arg == "--help" || arg == "-h") { + if command == "help" || command == "--help" || command == "-h" || command_args.is_empty() { return CommandOutput::new(render_app_help(&manifest), manifest_json(&manifest)) .print(app.output_mode); } + if is_help_requested(&command_args) { + let app_command = manifest + .commands + .iter() + .find(|candidate| candidate.name == command) + .ok_or_else(|| AppError::UnsupportedAppCommand { + command: command.clone(), + })?; + return CommandOutput::new( + render_app_command_help(&manifest, app_command), + app_command_json(&manifest, app_command), + ) + .print(app.output_mode); + } - let plan = plan_for_command(app, &manifest, &installed, &command_args).await?; + let runtime = AppRuntime::default(); + let result = runtime + .run_command( + app, + &manifest, + &installed, + &cache.module_path(&args.app, &installed.active_version), + &command_args, + ) + .await?; + let plan = match result { + GuestCommandResult::ActionPlan(plan) => plan, + GuestCommandResult::Output(output) => return output.print(app.output_mode), + }; + validate_guest_plan(app, &manifest, &installed, &command_args, &plan).await?; validate_plan_permissions(&manifest.permissions, &plan)?; let approval_required = plan_requires_approval(&plan); @@ -89,7 +117,7 @@ pub async fn run_app(app: &BeamApp, args: AppRunArgs) -> Result<()> { } approve_interactively(&render::render_plan(&plan))?; } - render_execution(&plan).print(app.output_mode) + execute_plan(app, &plan).await?.print(app.output_mode) } fn plan_requires_approval(plan: &ActionPlan) -> bool { @@ -105,6 +133,10 @@ fn filtered_app_args(args: &[String]) -> Vec { .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?; diff --git a/pkg/beam-cli/src/commands/apps/plans.rs b/pkg/beam-cli/src/commands/apps/plans.rs index 7bcc459..7860340 100644 --- a/pkg/beam-cli/src/commands/apps/plans.rs +++ b/pkg/beam-cli/src/commands/apps/plans.rs @@ -5,27 +5,73 @@ use crate::{ ActionPlan, ActionStep, AppManifest, AppPermissions, ChainOperation, InstalledApp, }, permissions::ensure_chain_scope, + store::now, }, error::Result, runtime::BeamApp, }; -pub(super) async fn plan_for_command( - _app: &BeamApp, +pub(super) async fn validate_guest_plan( + app: &BeamApp, manifest: &AppManifest, - _installed: &InstalledApp, - args: &[String], -) -> Result { - match (manifest.id.as_str(), args.first().map(String::as_str)) { - (_, Some(command)) => Err(AppError::UnsupportedAppCommand { - command: command.to_string(), + installed: &InstalledApp, + command_args: &[String], + plan: &ActionPlan, +) -> Result<()> { + let expected_command = command_args + .first() + .ok_or_else(|| AppError::InvalidGuestOutput { + reason: "guest plan missing command".to_string(), + })?; + if !plan.command.starts_with(expected_command) { + return Err(AppError::InvalidGuestOutput { + reason: format!( + "guest plan command `{}` does not match invocation `{expected_command}`", + plan.command + ), + } + .into()); + } + if plan.app_id != manifest.id + || plan.app_version != installed.active_version + || plan.manifest_sha256 != installed.manifest_sha256 + || plan.wasm_sha256 != installed.module_sha256 + { + return Err(AppError::InvalidGuestOutput { + reason: "guest plan artifact identity does not match installed app".to_string(), + } + .into()); + } + if plan.expires_at <= now() { + return Err(AppError::InvalidGuestOutput { + reason: "guest plan is already expired".to_string(), } - .into()), - (_, None) => Err(AppError::UnsupportedAppCommand { - command: "".to_string(), + .into()); + } + + let active_chain = app.active_chain().await?; + if plan.chain != active_chain.entry.key { + return Err(AppError::InvalidGuestOutput { + reason: format!( + "guest plan chain `{}` does not match active chain `{}`", + plan.chain, active_chain.entry.key + ), + } + .into()); + } + if let Some(wallet) = plan.wallet.as_ref() { + let active_wallet = format!("{:#x}", app.active_address().await?); + if !wallet.eq_ignore_ascii_case(&active_wallet) { + return Err(AppError::InvalidGuestOutput { + reason: format!( + "guest plan wallet `{wallet}` does not match active wallet `{active_wallet}`" + ), + } + .into()); } - .into()), } + + Ok(()) } pub(super) fn validate_plan_permissions( diff --git a/pkg/beam-cli/src/commands/apps/render.rs b/pkg/beam-cli/src/commands/apps/render.rs index e75b35c..6efc63f 100644 --- a/pkg/beam-cli/src/commands/apps/render.rs +++ b/pkg/beam-cli/src/commands/apps/render.rs @@ -1,7 +1,11 @@ +// lint-long-file-override allow-max-lines=300 use serde_json::{Value, json}; use crate::{ - apps::model::{ActionPlan, AppManifest, AppPermissions, ApprovalRecord}, + apps::model::{ + ActionPlan, AppCommand, AppCommandExample, AppCommandParameter, AppManifest, + AppPermissions, ApprovalRecord, + }, output::CommandOutput, }; @@ -87,6 +91,83 @@ pub(super) fn render_app_help(manifest: &AppManifest) -> String { lines.join("\n") } +pub(super) fn render_app_command_help(manifest: &AppManifest, command: &AppCommand) -> String { + let mut lines = Vec::new(); + lines.push(format!("{} {}", manifest.display_name, command.name)); + lines.push(command.about.clone()); + lines.push(String::new()); + lines.push(format!( + "Usage: {}", + command_usage(command).unwrap_or_else(|| command.name.clone()) + )); + + if let Some(docs) = &command.docs { + push_parameters(&mut lines, "Arguments", &docs.arguments); + push_parameters(&mut lines, "Options", &docs.options); + push_examples(&mut lines, &docs.examples); + if !docs.output_notes.is_empty() { + lines.push(String::new()); + lines.push("Output:".to_string()); + for note in &docs.output_notes { + lines.push(format!(" - {note}")); + } + } + } + + lines.join("\n") +} + +pub(super) fn app_command_json(manifest: &AppManifest, command: &AppCommand) -> Value { + json!({ + "app": manifest.id, + "command": command, + }) +} + +fn command_usage(command: &AppCommand) -> Option { + command + .docs + .as_ref() + .map(|docs| docs.invocation.clone()) + .or_else(|| (!command.usage.is_empty()).then(|| command.usage.clone())) +} + +fn push_parameters(lines: &mut Vec, title: &str, parameters: &[AppCommandParameter]) { + if parameters.is_empty() { + return; + } + lines.push(String::new()); + lines.push(format!("{title}:")); + for parameter in parameters { + let value_name = parameter + .value_name + .as_ref() + .map(|value| format!(" <{value}>")) + .unwrap_or_default(); + let required = if parameter.required { + "required" + } else { + "optional" + }; + lines.push(format!( + " - {}{} ({required}): {}", + parameter.name, value_name, parameter.description + )); + } +} + +fn push_examples(lines: &mut Vec, examples: &[AppCommandExample]) { + if examples.is_empty() { + return; + } + lines.push(String::new()); + lines.push("Examples:".to_string()); + for example in examples { + lines.push(format!(" - {}: {}", example.title, example.command)); + lines.push(format!(" {}", example.description)); + } +} + pub(super) fn render_plan(plan: &ActionPlan) -> String { let mut lines = vec![ format!("App: {} {}", plan.app_id, plan.app_version), @@ -121,19 +202,6 @@ pub(super) fn render_approval_created(record: &ApprovalRecord) -> CommandOutput ) } -pub(super) fn render_execution(plan: &ActionPlan) -> CommandOutput { - CommandOutput::new( - format!("Executed app action: {}", plan.command), - json!({ - "app": plan.app_id, - "chain": plan.chain, - "command": plan.command, - "state": "executed", - "steps": plan.steps, - }), - ) -} - pub(super) fn render_permission_diff(current: &AppManifest, next: &AppManifest) -> String { format!( "Update {} {} -> {} changes permissions.\n\nCurrent:\n{}\n\nNext:\n{}", @@ -163,3 +231,6 @@ pub(super) fn permissions_json(permissions: &AppPermissions) -> Value { pub(super) fn approval_json(approval: &ApprovalRecord) -> Value { serde_json::to_value(approval).unwrap_or_else(|_| json!({})) } + +#[cfg(test)] +mod tests; diff --git a/pkg/beam-cli/src/commands/apps/render/tests.rs b/pkg/beam-cli/src/commands/apps/render/tests.rs new file mode 100644 index 0000000..3200012 --- /dev/null +++ b/pkg/beam-cli/src/commands/apps/render/tests.rs @@ -0,0 +1,68 @@ +use crate::apps::model::{ + AppCatalogMetadata, AppCommand, AppCommandDocs, AppCommandExample, AppCommandParameter, + AppManifest, AppPermissions, HostApi, RegistrySignature, WasmArtifact, +}; + +use super::render_app_command_help; + +#[test] +fn command_help_renders_manifest_command_details() { + let manifest = AppManifest { + catalog: AppCatalogMetadata::default(), + commands: vec![AppCommand { + about: "Fetch a quote and prepare a swap".to_string(), + docs: Some(AppCommandDocs { + arguments: vec![AppCommandParameter { + default: None, + description: "Token to spend".to_string(), + kind: "string".to_string(), + name: "sell-token".to_string(), + required: true, + sensitive: false, + value_name: None, + }], + examples: vec![AppCommandExample { + command: "beam x uniswap swap USDC ETH 100".to_string(), + description: "Prepare a Base swap".to_string(), + title: "Swap".to_string(), + }], + invocation: "beam x uniswap swap ".to_string(), + options: Vec::new(), + output_notes: vec!["Returns a pending approval".to_string()], + summary: "Prepare a swap".to_string(), + }), + input_schema: serde_json::json!({}), + name: "swap".to_string(), + output_schema: serde_json::json!({}), + sensitive_args: Vec::new(), + usage: "swap ".to_string(), + }], + description: "Uniswap app".to_string(), + display_name: "Uniswap".to_string(), + format_version: 1, + host_api: HostApi::default(), + icon: None, + id: "uniswap".to_string(), + min_beam_version: "0.1.2".to_string(), + permissions: AppPermissions::default(), + publisher: "Payy".to_string(), + signature: RegistrySignature { + algorithm: "sha256-dev".to_string(), + key_id: "test".to_string(), + value: "sha256:0".to_string(), + }, + version: "1.0.0".to_string(), + wasm: WasmArtifact { + entrypoint: "beam_app_main".to_string(), + sha256: "sha256:0".to_string(), + }, + }; + + let help = render_app_command_help(&manifest, &manifest.commands[0]); + + assert!(help.contains("Usage: beam x uniswap swap")); + assert!(help.contains("Arguments:")); + assert!(help.contains("sell-token")); + assert!(help.contains("Examples:")); + assert!(help.contains("Returns a pending approval")); +} diff --git a/pkg/beam-cli/src/tests/apps_host.rs b/pkg/beam-cli/src/tests/apps_host.rs index b8c7b61..6b790cf 100644 --- a/pkg/beam-cli/src/tests/apps_host.rs +++ b/pkg/beam-cli/src/tests/apps_host.rs @@ -1,4 +1,7 @@ // lint-long-file-override allow-max-lines=300 +use std::path::{Path, PathBuf}; + +use super::fixtures::test_app; use crate::apps::{ approvals::{ensure_approval_executable, plan_hash}, host::{ @@ -7,11 +10,12 @@ use crate::apps::{ }, model::{ ActionBinding, ActionPlan, AppPermissions, ApprovalRecord, ApprovalStatus, ChainOperation, - ChainPermission, HttpPermission, + ChainPermission, HttpPermission, InstalledApp, RegistryIndex, }, - runtime::validate_wasm_module, + runtime::{AppRuntime, validate_wasm_module}, store::now, }; +use crate::runtime::InvocationOverrides; #[test] fn host_http_permissions_allow_declared_https_and_reject_private_hosts() { @@ -122,15 +126,49 @@ fn host_transaction_permissions_allow_broad_optional_globs() { #[test] fn app_runtime_requires_declared_entrypoint() { - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join("beam-apps/fixtures/valid/apps/uniswap/1.0.0/module.wasm"); + let path = repo_root().join("beam-apps/fixtures/valid/apps/uniswap/1.0.0/module.wasm"); validate_wasm_module("uniswap", "beam_app_main", &path).expect("valid app wasm"); validate_wasm_module("uniswap", "missing_entrypoint", &path) .expect_err("reject missing entrypoint"); } +#[tokio::test] +async fn app_runtime_invokes_guest_and_returns_structured_errors() { + let (_temp_dir, app) = test_app(InvocationOverrides { + chain: Some("base".to_string()), + from: Some("0x1111111111111111111111111111111111111111".to_string()), + ..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 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(), + installed_at: now(), + manifest_sha256: version.manifest_sha256.clone(), + module_sha256: version.module_sha256.clone(), + }; + + let error = AppRuntime::default() + .run_command( + &app, + &manifest, + &installed, + &module_path, + &["unknown".to_string()], + ) + .await + .expect_err("guest should reject unknown command"); + + assert!(error.to_string().contains("unsupported command")); +} + #[test] fn approval_integrity_rejects_tampered_plan() { let mut plan = action_plan(); @@ -193,3 +231,16 @@ fn action_plan() -> ActionPlan { expires_at: now() + 60, } } + +fn repo_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn read_json(path: &Path) -> T { + serde_json::from_slice(&std::fs::read(path).expect("read json")).expect("decode json") +} + +fn artifact_path(bundle: &Path, url: &str) -> PathBuf { + let prefix = "https://registry.beam.payy.network/"; + bundle.join(url.strip_prefix(prefix).expect("registry url")) +} diff --git a/pkg/json-store/src/lib.rs b/pkg/json-store/src/lib.rs index dfce8ab..7504ee0 100644 --- a/pkg/json-store/src/lib.rs +++ b/pkg/json-store/src/lib.rs @@ -39,14 +39,19 @@ //! } //! ``` +use std::ffi::OsString; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; +use std::process; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use tokio::fs; #[cfg(unix)] +use tokio::fs::OpenOptions; use tokio::io::AsyncWriteExt; use tokio::sync::RwLock; use tracing::{debug, info, warn}; @@ -198,7 +203,12 @@ where data.clone() } - /// Updates the state using a closure and persists the changes atomically + /// Updates the state using a closure and persists the changes atomically. + /// + /// The updated value is written and renamed into place before the + /// in-memory state is swapped. If persistence fails, callers receive the + /// error and subsequent reads from the same store instance continue to see + /// the last committed value. /// /// # Arguments /// * `update_fn` - A closure that takes a mutable reference to the state @@ -209,15 +219,20 @@ where where F: FnOnce(&mut T) + Send, { - { - let mut data = self.data.write().await; - update_fn(&mut *data); - } - - self.persist().await + let mut data = self.data.write().await; + let mut new_state = data.clone(); + update_fn(&mut new_state); + self.persist_state(&new_state).await?; + *data = new_state; + Ok(()) } - /// Replaces the entire state with a new value and persists it atomically + /// Replaces the entire state with a new value and persists it atomically. + /// + /// The replacement becomes visible through [`Self::get`] only after the + /// JSON file write and atomic rename succeed. This gives callers + /// transaction-like rollback semantics for a single store instance when the + /// filesystem rejects the write. /// /// # Arguments /// * `new_state` - The new state to replace the current one @@ -225,12 +240,36 @@ where /// # Returns /// Result indicating success or failure of the set operation pub async fn set(&self, new_state: T) -> Result<(), JsonStoreError> { - { - let mut data = self.data.write().await; - *data = new_state; - } + let mut data = self.data.write().await; + self.persist_state(&new_state).await?; + *data = new_state; + Ok(()) + } - self.persist().await + /// Replace state, or recover to a caller-supplied state if persistence fails. + /// + /// Higher-level stores use this when a failed final commit must abandon a + /// prepared in-memory attempt rather than exposing stale pending state. The + /// recovery state is also persisted when the filesystem permits it. If both + /// writes fail, the same store instance still swaps to `recovery_state` so + /// subsequent in-process reads observe the caller's rollback decision. + pub async fn set_with_recovery_on_error( + &self, + new_state: T, + recovery_state: T, + ) -> Result<(), JsonStoreError> { + let mut data = self.data.write().await; + match self.persist_state(&new_state).await { + Ok(()) => { + *data = new_state; + Ok(()) + } + Err(error) => { + let _ = self.persist_state(&recovery_state).await; + *data = recovery_state; + Err(error) + } + } } /// Persists the current state to the JSON file atomically @@ -238,19 +277,21 @@ where /// This method writes to a temporary file first, then atomically moves it /// to the target location to ensure consistency. async fn persist(&self) -> Result<(), JsonStoreError> { - let data = self.data.read().await; + let data = self.data.read().await.clone(); + self.persist_state(&data).await + } + async fn persist_state(&self, data: &T) -> Result<(), JsonStoreError> { // Serialize the data - let json_content = serde_json::to_string_pretty(&*data)?; - - // Create a temporary file in the same directory as the target file - let temp_path = self.file_path.with_extension("tmp"); + let json_content = serde_json::to_string_pretty(data)?; - // Write to temporary file - write_json_file(&temp_path, &json_content, self.file_access).await?; + let temp_path = + write_json_temp_file(&self.file_path, &json_content, self.file_access).await?; - // Atomically move the temporary file to the target location - fs::rename(&temp_path, &self.file_path).await?; + if let Err(error) = fs::rename(&temp_path, &self.file_path).await { + let _ = fs::remove_file(&temp_path).await; + return Err(error.into()); + } ensure_file_access(&self.file_path, self.file_access).await?; debug!("Successfully persisted state to: {:?}", self.file_path); @@ -276,33 +317,81 @@ where } } -async fn write_json_file( - path: &Path, +static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0); + +async fn write_json_temp_file( + target_path: &Path, content: &str, file_access: FileAccess, -) -> Result<(), std::io::Error> { - if matches!(file_access, FileAccess::OwnerOnly) { - return write_owner_only_json_file(path, content).await; +) -> Result { + for _ in 0..16 { + let temp_path = unique_temp_path(target_path)?; + let result = if matches!(file_access, FileAccess::OwnerOnly) { + write_owner_only_json_file(&temp_path, content).await + } else { + write_shared_json_file(&temp_path, content).await + }; + match result { + Ok(()) => return Ok(temp_path), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => { + let _ = fs::remove_file(&temp_path).await; + return Err(error); + } + } } - fs::write(path, content).await + Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + "could not create a unique json-store temp file", + )) +} + +fn unique_temp_path(target_path: &Path) -> Result { + let parent = target_path.parent().unwrap_or_else(|| Path::new(".")); + let file_name = target_path.file_name().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "missing json-store file name", + ) + })?; + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |duration| duration.as_nanos()); + let counter = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed); + let mut temp_name = OsString::from("."); + temp_name.push(file_name); + temp_name.push(format!(".tmp-{}-{nanos}-{counter}", process::id())); + + Ok(parent.join(temp_name)) } #[cfg(unix)] async fn write_owner_only_json_file(path: &Path, content: &str) -> Result<(), std::io::Error> { - let mut options = fs::OpenOptions::new(); - options.create(true).truncate(true).write(true).mode(0o600); + let mut options = OpenOptions::new(); + options.create_new(true).write(true).mode(0o600); - let mut file = options.open(path).await?; - file.write_all(content.as_bytes()).await?; - file.flush().await?; - drop(file); + { + let mut file = options.open(path).await?; + file.write_all(content.as_bytes()).await?; + file.flush().await?; + } ensure_file_access(path, FileAccess::OwnerOnly).await } #[cfg(not(unix))] async fn write_owner_only_json_file(path: &Path, content: &str) -> Result<(), std::io::Error> { - fs::write(path, content).await + write_shared_json_file(path, content).await +} + +async fn write_shared_json_file(path: &Path, content: &str) -> Result<(), std::io::Error> { + let mut file = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(path) + .await?; + file.write_all(content.as_bytes()).await?; + file.flush().await } async fn ensure_file_access(path: &Path, file_access: FileAccess) -> Result<(), std::io::Error> { diff --git a/pkg/json-store/src/tests.rs b/pkg/json-store/src/tests.rs index 7eeb217..13f6030 100644 --- a/pkg/json-store/src/tests.rs +++ b/pkg/json-store/src/tests.rs @@ -3,6 +3,7 @@ use super::*; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; +use serde::ser::{Error as SerializeError, SerializeStruct}; use serde::{Deserialize, Serialize}; use tempdir::TempDir; @@ -13,6 +14,28 @@ struct TestState { active: bool, } +#[derive(Debug, Clone, Deserialize, Default, PartialEq)] +struct SometimesFailingState { + counter: u64, + fail_serialize: bool, +} + +impl Serialize for SometimesFailingState { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.fail_serialize { + return Err(S::Error::custom("forced serialization failure")); + } + + let mut state = serializer.serialize_struct("SometimesFailingState", 2)?; + state.serialize_field("counter", &self.counter)?; + state.serialize_field("fail_serialize", &self.fail_serialize)?; + state.end() + } +} + #[tokio::test] async fn test_new_store_creates_default_state() { let temp_dir = TempDir::new("json_kv_store_test").unwrap(); @@ -65,6 +88,52 @@ async fn test_set_and_persist() { assert_eq!(retrieved_state, new_state); } +#[tokio::test] +async fn set_persist_failure_does_not_change_in_memory_state() { + let temp_dir = TempDir::new("json_kv_store_test").unwrap(); + let store = JsonStore::::new(temp_dir.path(), "set_failure.json") + .await + .unwrap(); + let committed = SometimesFailingState { + counter: 1, + fail_serialize: false, + }; + store.set(committed.clone()).await.unwrap(); + + let result = store + .set(SometimesFailingState { + counter: 2, + fail_serialize: true, + }) + .await; + + assert!(matches!(result, Err(JsonStoreError::Serialization(_)))); + assert_eq!(store.get().await, committed); +} + +#[tokio::test] +async fn update_persist_failure_does_not_change_in_memory_state() { + let temp_dir = TempDir::new("json_kv_store_test").unwrap(); + let store = JsonStore::::new(temp_dir.path(), "update_failure.json") + .await + .unwrap(); + let committed = SometimesFailingState { + counter: 1, + fail_serialize: false, + }; + store.set(committed.clone()).await.unwrap(); + + let result = store + .update(|state| { + state.counter = 2; + state.fail_serialize = true; + }) + .await; + + assert!(matches!(result, Err(JsonStoreError::Serialization(_)))); + assert_eq!(store.get().await, committed); +} + #[tokio::test] async fn test_persistence_across_instances() { let temp_dir = TempDir::new("json_kv_store_test").unwrap(); @@ -201,6 +270,11 @@ async fn test_owner_only_access_restricts_existing_and_persisted_files() { 0o600 ); + let fixed_temp_path = store.file_path.with_extension("tmp"); + fs::write(&fixed_temp_path, "preexisting fixed temp path") + .await + .unwrap(); + store .update(|state| { state.counter = 1; @@ -212,4 +286,8 @@ async fn test_owner_only_access_restricts_existing_and_persisted_files() { std::fs::metadata(&file_path).unwrap().permissions().mode() & 0o777, 0o600 ); + assert_eq!( + fs::read_to_string(fixed_temp_path).await.unwrap(), + "preexisting fixed temp path" + ); } diff --git a/pkg/workspace-hack/Cargo.toml b/pkg/workspace-hack/Cargo.toml index 06144a6..08a07b3 100644 --- a/pkg/workspace-hack/Cargo.toml +++ b/pkg/workspace-hack/Cargo.toml @@ -11,6 +11,7 @@ default = [] ### BEGIN HAKARI SECTION [dependencies] actix-router = { version = "0.5", default-features = false, features = ["http", "unicode"] } +aead = { version = "0.5", features = ["alloc", "getrandom"] } ahash = { version = "0.8", default-features = false, features = ["runtime-rng"] } aho-corasick = { version = "1" } allocator-api2 = { version = "0.2" } @@ -38,6 +39,7 @@ ark-serialize = { version = "0.5", default-features = false, features = ["derive ark-std = { version = "0.5", default-features = false, features = ["std"] } arrayvec = { version = "0.7", features = ["serde"] } async-compression = { version = "0.4", default-features = false, features = ["brotli", "gzip", "tokio", "zlib", "zstd"] } +axum = { version = "0.7", features = ["macros"] } base64 = { version = "0.13", features = ["alloc"] } bitflags = { version = "2", default-features = false, features = ["serde", "std"] } bitvec = { version = "1", features = ["serde"] } @@ -61,6 +63,7 @@ crypto-common = { version = "0.1", default-features = false, features = ["getran curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "digest", "precomputed-tables", "zeroize"] } dashmap = { version = "6", default-features = false, features = ["inline"] } data-encoding = { version = "2" } +der = { version = "0.7", default-features = false, features = ["oid", "pem", "std"] } derive_more = { version = "2", features = ["full"] } digest = { version = "0.10", features = ["mac", "oid", "std"] } ecdsa = { version = "0.16", default-features = false, features = ["pem", "serde", "signing", "std", "verifying"] } @@ -122,6 +125,7 @@ num-traits = { version = "0.2", features = ["i128", "libm"] } num_enum = { version = "0.7" } nybbles = { version = "0.4", default-features = false, features = ["rlp", "serde", "std"] } once_cell = { version = "1", features = ["critical-section"] } +p256 = { version = "0.13", features = ["ecdh"] } parity-scale-codec = { version = "3", features = ["bytes", "derive", "max-encoded-len"] } parking_lot = { version = "0.12", features = ["arc_lock", "send_guard", "serde"] } percent-encoding = { version = "2" } @@ -134,7 +138,7 @@ proc-macro2 = { version = "1", features = ["span-locations"] } prost = { version = "0.13", features = ["prost-derive"] } rand-274715c4dabd11b0 = { package = "rand", version = "0.9", features = ["serde"] } rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["serde", "small_rng"] } -rand_chacha-274715c4dabd11b0 = { package = "rand_chacha", version = "0.9", default-features = false, features = ["std"] } +rand_chacha-274715c4dabd11b0 = { package = "rand_chacha", version = "0.9" } rand_chacha-468e82937335b1c9 = { package = "rand_chacha", version = "0.3" } rand_core-274715c4dabd11b0 = { package = "rand_core", version = "0.9", default-features = false, features = ["os_rng", "serde", "std"] } rand_core-3b31131e45eafb45 = { package = "rand_core", version = "0.6", default-features = false, features = ["std"] } @@ -155,7 +159,7 @@ sec1 = { version = "0.7", features = ["pem", "serde", "std", "subtle"] } semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["alloc", "derive", "rc"] } serde_core = { version = "1", features = ["alloc", "rc"] } -serde_json = { version = "1", features = ["alloc", "raw_value", "unbounded_depth"] } +serde_json = { version = "1", features = ["alloc", "float_roundtrip", "raw_value", "unbounded_depth"] } serde_spanned = { version = "1", default-features = false, features = ["serde", "std"] } serde_with = { version = "3", features = ["base64"] } sha1 = { version = "0.10", features = ["oid"] } @@ -167,6 +171,7 @@ smallvec = { version = "1", default-features = false, features = ["const_new", " socket2-3b31131e45eafb45 = { package = "socket2", version = "0.6", default-features = false, features = ["all"] } socket2-d8f496e17d97b5cb = { package = "socket2", version = "0.5", default-features = false, features = ["all"] } spin = { version = "0.9", default-features = false, features = ["once", "rwlock", "spin_mutex", "std"] } +spki = { version = "0.7", default-features = false, features = ["pem", "std"] } strum = { version = "0.27", features = ["derive"] } subtle = { version = "2" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } @@ -189,6 +194,7 @@ unicode-normalization = { version = "0.1" } url = { version = "2", features = ["serde"] } uuid = { version = "1", features = ["serde", "v4"] } winnow = { version = "0.7" } +x25519-dalek = { version = "2", features = ["static_secrets"] } zeroize = { version = "1", features = ["zeroize_derive"] } zstd = { version = "0.13", features = ["experimental"] } zstd-safe = { version = "7", default-features = false, features = ["arrays", "experimental", "legacy", "std", "zdict_builder"] } @@ -196,6 +202,7 @@ zstd-sys = { version = "2", features = ["experimental", "std"] } [build-dependencies] actix-router = { version = "0.5", default-features = false, features = ["http", "unicode"] } +aead = { version = "0.5", features = ["alloc", "getrandom"] } ahash = { version = "0.8", default-features = false, features = ["runtime-rng"] } aho-corasick = { version = "1" } allocator-api2 = { version = "0.2" } @@ -226,6 +233,7 @@ ark-serialize = { version = "0.5", default-features = false, features = ["derive ark-std = { version = "0.5", default-features = false, features = ["std"] } arrayvec = { version = "0.7", features = ["serde"] } async-compression = { version = "0.4", default-features = false, features = ["brotli", "gzip", "tokio", "zlib", "zstd"] } +axum = { version = "0.7", features = ["macros"] } base64 = { version = "0.13", features = ["alloc"] } bindgen = { version = "0.71" } bitflags = { version = "2", default-features = false, features = ["serde", "std"] } @@ -254,6 +262,7 @@ darling = { version = "0.21", features = ["serde"] } darling_core = { version = "0.21", default-features = false, features = ["serde", "suggestions"] } dashmap = { version = "6", default-features = false, features = ["inline"] } data-encoding = { version = "2" } +der = { version = "0.7", default-features = false, features = ["oid", "pem", "std"] } derive_more = { version = "2", features = ["full"] } derive_more-impl = { version = "2", features = ["add", "add_assign", "as_ref", "constructor", "debug", "deref", "deref_mut", "display", "eq", "error", "from", "from_str", "index", "index_mut", "into", "into_iterator", "is_variant", "mul", "mul_assign", "not", "sum", "try_from", "try_into", "try_unwrap", "unwrap"] } digest = { version = "0.10", features = ["mac", "oid", "std"] } @@ -317,6 +326,7 @@ num_enum = { version = "0.7" } num_enum_derive = { version = "0.7", default-features = false, features = ["std"] } nybbles = { version = "0.4", default-features = false, features = ["rlp", "serde", "std"] } once_cell = { version = "1", features = ["critical-section"] } +p256 = { version = "0.13", features = ["ecdh"] } parity-scale-codec = { version = "3", features = ["bytes", "derive", "max-encoded-len"] } parking_lot = { version = "0.12", features = ["arc_lock", "send_guard", "serde"] } percent-encoding = { version = "2" } @@ -331,7 +341,7 @@ prost = { version = "0.13", features = ["prost-derive"] } quote = { version = "1" } rand-274715c4dabd11b0 = { package = "rand", version = "0.9", features = ["serde"] } rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["serde", "small_rng"] } -rand_chacha-274715c4dabd11b0 = { package = "rand_chacha", version = "0.9", default-features = false, features = ["std"] } +rand_chacha-274715c4dabd11b0 = { package = "rand_chacha", version = "0.9" } rand_chacha-468e82937335b1c9 = { package = "rand_chacha", version = "0.3" } rand_core-274715c4dabd11b0 = { package = "rand_core", version = "0.9", default-features = false, features = ["os_rng", "serde", "std"] } rand_core-3b31131e45eafb45 = { package = "rand_core", version = "0.6", default-features = false, features = ["std"] } @@ -352,7 +362,7 @@ sec1 = { version = "0.7", features = ["pem", "serde", "std", "subtle"] } semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["alloc", "derive", "rc"] } serde_core = { version = "1", features = ["alloc", "rc"] } -serde_json = { version = "1", features = ["alloc", "raw_value", "unbounded_depth"] } +serde_json = { version = "1", features = ["alloc", "float_roundtrip", "raw_value", "unbounded_depth"] } serde_spanned = { version = "1", default-features = false, features = ["serde", "std"] } serde_with = { version = "3", features = ["base64"] } sha1 = { version = "0.10", features = ["oid"] } @@ -364,6 +374,7 @@ smallvec = { version = "1", default-features = false, features = ["const_new", " socket2-3b31131e45eafb45 = { package = "socket2", version = "0.6", default-features = false, features = ["all"] } socket2-d8f496e17d97b5cb = { package = "socket2", version = "0.5", default-features = false, features = ["all"] } spin = { version = "0.9", default-features = false, features = ["once", "rwlock", "spin_mutex", "std"] } +spki = { version = "0.7", default-features = false, features = ["pem", "std"] } strum = { version = "0.27", features = ["derive"] } subtle = { version = "2" } syn-dff4ba8e3ae991db = { package = "syn", version = "1", features = ["extra-traits", "full", "visit"] } @@ -388,6 +399,7 @@ unicode-normalization = { version = "0.1" } url = { version = "2", features = ["serde"] } uuid = { version = "1", features = ["serde", "v4"] } winnow = { version = "0.7" } +x25519-dalek = { version = "2", features = ["static_secrets"] } zeroize = { version = "1", features = ["zeroize_derive"] } zstd = { version = "0.13", features = ["experimental"] } zstd-safe = { version = "7", default-features = false, features = ["arrays", "experimental", "legacy", "std", "zdict_builder"] } diff --git a/pkg/xtask/src/lint/steps/checks.rs b/pkg/xtask/src/lint/steps/checks.rs index c28c17a..b12c623 100644 --- a/pkg/xtask/src/lint/steps/checks.rs +++ b/pkg/xtask/src/lint/steps/checks.rs @@ -1,3 +1,4 @@ +// lint-long-file-override allow-max-lines=240 use std::io::ErrorKind; use std::path::Path; use std::time::Instant; diff --git a/scripts/prepare-beam-release-pr.sh b/scripts/prepare-beam-release-pr.sh index 0dba887..cf52811 100755 --- a/scripts/prepare-beam-release-pr.sh +++ b/scripts/prepare-beam-release-pr.sh @@ -2,6 +2,7 @@ set -euo pipefail readonly BEAM_MANIFEST="pkg/beam-cli/Cargo.toml" +readonly BEAM_SITE_VERSION_FILE="app/packages/beam-site/src/lib/beamVersion.ts" readonly PAYY_REPO_URL="${PAYY_REPO_URL:-https://github.com/polybase/payy.git}" current_version() { @@ -133,6 +134,12 @@ update_manifest_version() { perl -0pi -e 's/^version = "[^"]+"/version = "$ENV{BEAM_NEXT_VERSION}"/m' "$BEAM_MANIFEST" } +update_site_version() { + local version="$1" + + BEAM_NEXT_VERSION="$version" perl -0pi -e "s/^const FALLBACK_BEAM_VERSION = '[^']+'/const FALLBACK_BEAM_VERSION = '\$ENV{BEAM_NEXT_VERSION}'/m" "$BEAM_SITE_VERSION_FILE" +} + create_or_update_pr() { local version="$1" local branch="beam/release-v${version}" @@ -154,14 +161,15 @@ EOF git checkout -B "$branch" BEAM_NEXT_VERSION="$version" update_manifest_version "$version" + update_site_version "$version" cargo update -p beam-cli - if git diff --quiet -- "$BEAM_MANIFEST" Cargo.lock; then + if git diff --quiet -- "$BEAM_MANIFEST" Cargo.lock "$BEAM_SITE_VERSION_FILE"; then echo "Beam release ${version} produced no manifest or lockfile changes." exit 0 fi - git add "$BEAM_MANIFEST" Cargo.lock + git add "$BEAM_MANIFEST" Cargo.lock "$BEAM_SITE_VERSION_FILE" git commit -m "$title" -m "$body" git fetch origin "refs/heads/${branch}:refs/remotes/origin/${branch}" || true git push --force-with-lease origin "$branch"