diff --git a/README.md b/README.md index 626490d..8c2c6e2 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,16 @@ hyperliquid --keystore ~/.foundry/keystores/my-wallet ... hyperliquid --private-key 0x... ... # avoid in shared shells / history ``` +Bankr can also be used as an external signer for supported payloads. Bankr account setup and API-key creation happen outside this CLI; `hyperliquid` only uses the Bankr Wallet API to resolve the attached EVM wallet and request signatures. + +```bash +export BANKR_API_KEY=bk_... +hyperliquid --bankr-signer default wallet address +hyperliquid --bankr-signer default --dry-run orders create --coin BTC --side buy --price 1 --size 0.001 +``` + +Bankr support is intentionally fail-closed for raw Hyperliquid L1 action hashes until Bankr raw digest signing support is confirmed and verified. Use OWS, a keystore, stored local signing account, or explicit private key for live order/trading commands that require raw L1 signing. + Or set environment variables: ```bash @@ -138,7 +148,7 @@ export OWS_PASSPHRASE=... # unlock encrypted OWS wallet ### Safety rules -- **Never** commit private keys, mnemonics, keystore files, OWS secrets, or config databases. +- **Never** commit private keys, mnemonics, keystore files, OWS secrets, Bankr API keys, or config databases. - Prefer OWS wallets or keystores over raw `--private-key` flags in shared environments. - Use API wallets when delegating trading to scripts or agents — they can't withdraw funds. - `--testnet` is one flag away whenever you want to rehearse a flow before going live. diff --git a/examples/bankr_live_smoke.rs b/examples/bankr_live_smoke.rs new file mode 100644 index 0000000..8da0ed3 --- /dev/null +++ b/examples/bankr_live_smoke.rs @@ -0,0 +1,57 @@ +use alloy::dyn_abi::TypedData; +use hyperliquid_cli::bankr; +use hyperliquid_cli::signing::SelectedSigner; +use hypersdk::hypercore::Chain; +use hypersdk::hypercore::api::UpdateLeverage; +use hypersdk::hypercore::types::Action; + +fn main() -> Result<(), Box> { + let config = bankr::resolve_selector("default")?; + println!("wallet_address={}", config.address()); + + let signer = SelectedSigner::bankr(config.clone()); + let err = signer + .sign_l1_action_sync( + Action::UpdateLeverage(UpdateLeverage { + asset: 0, + is_cross: true, + leverage: 2, + }), + 1_777_963_000_000, + None, + Chain::Testnet, + ) + .unwrap_err(); + assert_eq!(err.exit_code(), 13); + println!("raw_l1_fail_closed_ok=true"); + + let typed_data: TypedData = serde_json::from_value(serde_json::json!({ + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "chainId", "type": "uint256"} + ], + "BankrCliSmoke": [ + {"name": "message", "type": "string"}, + {"name": "nonce", "type": "uint256"} + ] + }, + "primaryType": "BankrCliSmoke", + "domain": { + "name": "Hyperliquid CLI Bankr Smoke", + "chainId": "0x3e7" + }, + "message": { + "message": "bankr signing smoke", + "nonce": "0x1" + } + }))?; + + let _typed_signature = bankr::sign_typed_data(&config, &typed_data)?; + println!("typed_data_signature_ok=true"); + + let _message_signature = bankr::sign_message(&config, b"hyperliquid-cli bankr live smoke")?; + println!("personal_sign_signature_ok=true"); + + Ok(()) +} diff --git a/src/auth.rs b/src/auth.rs index 78f491b..368bc50 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -121,6 +121,11 @@ pub fn resolve_signer_with_account_and_ows( resolve_stored_default_signer() } +pub fn resolve_bankr_signer(selector: &str) -> Result { + let bankr = crate::bankr::resolve_selector(selector)?; + Ok(ResolvedSigner::new(SelectedSigner::bankr(bankr))) +} + pub fn resolve_stored_account_signer(selector: &str) -> Result { // Try OWS first: selector may be an OWS wallet name or id. let vault_path = crate::ows::ows_vault_path(); diff --git a/src/bankr.rs b/src/bankr.rs new file mode 100644 index 0000000..f57f772 --- /dev/null +++ b/src/bankr.rs @@ -0,0 +1,872 @@ +//! Bankr Wallet API signer integration. +//! +//! Bankr is treated as an external signer source. The CLI resolves the EVM +//! wallet attached to a Bankr API key, asks Bankr to sign supported payloads, +//! and verifies the recovered signer address before accepting the signature. + +use std::env; +use std::future::Future; +use std::sync::OnceLock; +use std::time::Duration; + +use alloy::dyn_abi::TypedData; +use alloy_primitives::{Address as AlloyAddress, B256}; +use hypersdk::Address; +use hypersdk::hypercore::types::Signature as HyperliquidSignature; +use reqwest::StatusCode; +use serde::Deserialize; +use serde_json::Value; + +use crate::errors::{CliError, http_response_indicates_rate_limit}; +use crate::response_sanitization::labelled_untrusted_text; + +const DEFAULT_BANKR_API_BASE_URL: &str = "https://api.bankr.bot"; +const DEFAULT_SELECTOR: &str = "default"; +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); + +pub const BANKR_API_KEY_ENV: &str = "BANKR_API_KEY"; +pub const BANKR_API_BASE_URL_ENV: &str = "BANKR_API_BASE_URL"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BankrSigningConfig { + selector: String, + api_key: String, + api_base_url: String, + address: Address, +} + +impl BankrSigningConfig { + #[must_use] + pub fn new(selector: String, api_key: String, api_base_url: String, address: Address) -> Self { + Self { + selector, + api_key, + api_base_url, + address, + } + } + + #[must_use] + pub fn selector(&self) -> &str { + &self.selector + } + + #[must_use] + pub fn address(&self) -> Address { + self.address + } + + fn api_key(&self) -> &str { + &self.api_key + } + + fn api_base_url(&self) -> &str { + &self.api_base_url + } +} + +pub fn resolve_selector(selector: &str) -> Result { + let selector = selector.trim(); + if selector.is_empty() { + return Err(CliError::Configuration( + "--bankr-signer requires a non-empty selector".to_string(), + )); + } + if selector != DEFAULT_SELECTOR { + return Err(CliError::Unsupported(format!( + "bankr_unsupported_selector: only '{DEFAULT_SELECTOR}' is currently supported" + ))); + } + + let api_key = bankr_api_key()?; + let api_base_url = bankr_api_base_url(); + let address = wallet_address(&api_base_url, &api_key)?; + Ok(BankrSigningConfig::new( + selector.to_string(), + api_key, + api_base_url, + address, + )) +} + +pub fn unsupported_raw_l1_signing(selector: &str) -> CliError { + CliError::Unsupported(format!( + "Bankr signer '{selector}' cannot sign raw Hyperliquid L1 action hashes with the currently documented Bankr Wallet API; use OWS, a local signing account, keystore, or private key for this live action" + )) +} + +pub fn unsupported_local_private_key(selector: &str) -> CliError { + CliError::Unsupported(format!( + "Bankr signer '{selector}' does not expose a local private key; use a local signing account, keystore, or private key for this command" + )) +} + +pub fn sign_typed_data( + config: &BankrSigningConfig, + typed_data: &TypedData, +) -> Result { + let typed_data_value = bankr_typed_data_value(typed_data)?; + let response: SignResponse = post_json( + config.api_base_url(), + "/wallet/sign", + config.api_key(), + &serde_json::json!({ + "signatureType": "eth_signTypedData_v4", + "typedData": typed_data_value, + }), + "signing Bankr typed data", + )?; + let signature = hyperliquid_signature_from_bankr(response)?; + let alloy_signature: alloy_primitives::Signature = signature.into(); + let signing_hash = typed_data.eip712_signing_hash().map_err(|err| { + CliError::Internal(anyhow::anyhow!("failed to hash Bankr typed data: {err}")) + })?; + verify_recovered_address(config, &alloy_signature, signing_hash)?; + Ok(signature) +} + +pub fn sign_message( + config: &BankrSigningConfig, + message: &[u8], +) -> Result { + let message = std::str::from_utf8(message).map_err(|_| { + CliError::Unsupported( + "bankr_unsupported_message: Bankr personal_sign requires UTF-8 message bytes" + .to_string(), + ) + })?; + let response: SignResponse = post_json( + config.api_base_url(), + "/wallet/sign", + config.api_key(), + &serde_json::json!({ + "signatureType": "personal_sign", + "message": message, + }), + "signing Bankr message", + )?; + let signature = alloy_signature_from_bankr(response)?; + let recovered = signature + .recover_address_from_msg(message.as_bytes()) + .map_err(|err| { + CliError::InvalidAuth(format!( + "bankr_malformed_response: failed to recover Bankr message signer: {err}" + )) + })?; + verify_address_matches(config, recovered)?; + Ok(signature) +} + +fn wallet_address(api_base_url: &str, api_key: &str) -> Result { + let response: WalletMeResponse = get_json( + api_base_url, + "/wallet/me", + api_key, + "resolving Bankr wallet", + )?; + let address = response + .evm_address() + .ok_or_else(|| { + CliError::InvalidAuth( + "bankr_no_evm_wallet: Bankr wallet response did not include an EVM address" + .to_string(), + ) + })? + .parse::
() + .map_err(|_| { + CliError::InvalidAuth( + "bankr_malformed_response: Bankr returned an invalid EVM address".to_string(), + ) + })?; + Ok(address) +} + +fn bankr_http_client() -> Result<&'static reqwest::Client, CliError> { + static CLIENT: OnceLock> = OnceLock::new(); + CLIENT + .get_or_init(|| { + reqwest::Client::builder() + .timeout(DEFAULT_TIMEOUT) + .build() + .map_err(|err| err.to_string()) + }) + .as_ref() + .map_err(|err| { + CliError::Internal(anyhow::anyhow!("failed to build Bankr HTTP client: {err}")) + }) +} + +/// Run async Bankr HTTP from sync call sites (CLI signer resolution, examples, tests). +fn run_bankr_async(f: F) -> Result +where + F: FnOnce() -> Fut, + Fut: Future>, + T: Send, +{ + match tokio::runtime::Handle::try_current() { + Ok(handle) => tokio::task::block_in_place(|| handle.block_on(f())), + Err(_) => tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|err| CliError::Internal(anyhow::anyhow!(err)))? + .block_on(f()), + } +} + +fn get_json Deserialize<'de> + Send>( + api_base_url: &str, + path: &str, + api_key: &str, + context: &'static str, +) -> Result { + let api_base_url = api_base_url.to_string(); + let api_key = api_key.to_string(); + run_bankr_async( + move || async move { get_json_async(&api_base_url, path, &api_key, context).await }, + ) +} + +async fn get_json_async Deserialize<'de> + Send>( + api_base_url: &str, + path: &str, + api_key: &str, + context: &'static str, +) -> Result { + let url = bankr_url(api_base_url, path)?; + let response = bankr_http_client()? + .get(url) + .header("X-API-Key", api_key) + .send() + .await + .map_err(|err| CliError::Unavailable(format!("Check your network connection. {err}")))?; + decode_response(response, context).await +} + +fn post_json Deserialize<'de> + Send>( + api_base_url: &str, + path: &str, + api_key: &str, + payload: &Value, + context: &'static str, +) -> Result { + let api_base_url = api_base_url.to_string(); + let api_key = api_key.to_string(); + let payload = payload.clone(); + run_bankr_async(move || async move { + post_json_async(&api_base_url, path, &api_key, &payload, context).await + }) +} + +async fn post_json_async Deserialize<'de> + Send>( + api_base_url: &str, + path: &str, + api_key: &str, + payload: &Value, + context: &'static str, +) -> Result { + let url = bankr_url(api_base_url, path)?; + let response = bankr_http_client()? + .post(url) + .header("X-API-Key", api_key) + .json(payload) + .send() + .await + .map_err(|err| CliError::Unavailable(format!("Check your network connection. {err}")))?; + decode_response(response, context).await +} + +async fn decode_response Deserialize<'de> + Send>( + response: reqwest::Response, + context: &'static str, +) -> Result { + let status = response.status(); + let body = response + .text() + .await + .map_err(|err| CliError::Unavailable(format!("Failed to read Bankr response. {err}")))?; + ensure_bankr_success(status, &body)?; + serde_json::from_str::(&body).map_err(|err| { + let body = labelled_untrusted_text(&body); + CliError::InvalidAuth(format!( + "bankr_malformed_response: decode failed while {context}: {err}; body={body}" + )) + }) +} + +fn ensure_bankr_success(status: StatusCode, body: &str) -> Result<(), CliError> { + if http_response_indicates_rate_limit(status.as_u16(), body) { + return Err(CliError::RateLimited); + } + if status == StatusCode::UNAUTHORIZED { + return Err(CliError::InvalidAuth(bankr_error_message( + "bankr_auth_required", + body, + ))); + } + if status == StatusCode::FORBIDDEN { + return Err(CliError::InvalidAuth(bankr_error_message( + "bankr_access_denied", + body, + ))); + } + if status == StatusCode::BAD_REQUEST { + return Err(CliError::InvalidAuth(bankr_error_message( + "bankr_bad_request", + body, + ))); + } + if !status.is_success() { + return Err(CliError::Unavailable(bankr_error_message( + "bankr_unavailable", + body, + ))); + } + if let Ok(value) = serde_json::from_str::(body) + && value.get("success") == Some(&Value::Bool(false)) + { + return Err(CliError::InvalidAuth(bankr_error_message( + "bankr_sign_failed", + body, + ))); + } + Ok(()) +} + +fn bankr_error_message(prefix: &'static str, body: &str) -> String { + let body = labelled_untrusted_text(body); + format!("{prefix}: {body}") +} + +fn bankr_url(api_base_url: &str, path: &str) -> Result { + let mut url = reqwest::Url::parse(api_base_url) + .map_err(|err| CliError::Configuration(format!("invalid Bankr API base URL: {err}")))?; + url.set_path(path); + url.set_query(None); + Ok(url) +} + +fn bankr_api_key() -> Result { + env::var(BANKR_API_KEY_ENV) + .ok() + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + CliError::InvalidAuth(format!( + "{BANKR_API_KEY_ENV} is required when using --bankr-signer" + )) + }) +} + +fn bankr_api_base_url() -> String { + env::var(BANKR_API_BASE_URL_ENV) + .ok() + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_BANKR_API_BASE_URL.to_string()) +} + +fn bankr_typed_data_value(typed_data: &TypedData) -> Result { + let mut value = serde_json::to_value(typed_data).map_err(|err| { + CliError::Internal(anyhow::anyhow!("failed to encode Bankr typed data: {err}")) + })?; + ensure_eip712_domain_type(&mut value); + pad_odd_hex_typed_values(&mut value); + normalize_bankr_numeric_fields(&mut value); + Ok(value) +} + +fn ensure_eip712_domain_type(value: &mut Value) { + let Some(domain) = value.get("domain").and_then(Value::as_object).cloned() else { + return; + }; + let Some(types) = value.get_mut("types").and_then(Value::as_object_mut) else { + return; + }; + if types.contains_key("EIP712Domain") { + return; + } + + let mut fields = Vec::new(); + for (name, type_name) in [ + ("name", "string"), + ("version", "string"), + ("chainId", "uint256"), + ("verifyingContract", "address"), + ("salt", "bytes32"), + ] { + if domain.contains_key(name) { + fields.push(serde_json::json!({"name": name, "type": type_name})); + } + } + types.insert("EIP712Domain".to_string(), Value::Array(fields)); +} + +fn pad_odd_hex_typed_values(value: &mut Value) { + let Some(types) = value.get("types").and_then(Value::as_object).cloned() else { + return; + }; + if let Some(domain) = value.get_mut("domain") { + pad_struct_odd_hex_values("EIP712Domain", domain, &types); + } + let Some(primary_type) = value + .get("primaryType") + .and_then(Value::as_str) + .map(str::to_string) + else { + return; + }; + if let Some(message) = value.get_mut("message") { + pad_struct_odd_hex_values(&primary_type, message, &types); + } +} + +fn pad_struct_odd_hex_values( + type_name: &str, + value: &mut Value, + types: &serde_json::Map, +) { + let Some(fields) = types.get(type_name).and_then(Value::as_array).cloned() else { + return; + }; + let Some(object) = value.as_object_mut() else { + return; + }; + + for field in fields { + let Some(name) = field.get("name").and_then(Value::as_str) else { + continue; + }; + let Some(field_type) = field.get("type").and_then(Value::as_str) else { + continue; + }; + let Some(field_value) = object.get_mut(name) else { + continue; + }; + pad_field_odd_hex_values(field_type, field_value, types); + } +} + +fn pad_field_odd_hex_values( + field_type: &str, + value: &mut Value, + types: &serde_json::Map, +) { + if let Some(element_type) = field_type.strip_suffix("[]") { + if let Some(values) = value.as_array_mut() { + for value in values { + pad_field_odd_hex_values(element_type, value, types); + } + } + return; + } + + if types.contains_key(field_type) { + pad_struct_odd_hex_values(field_type, value, types); + return; + } + + if is_hex_encoded_eip712_scalar(field_type) + && let Some(text) = value.as_str() + && let Some(hex) = text.strip_prefix("0x") + && hex.len() % 2 == 1 + { + *value = Value::String(format!("0x0{hex}")); + } +} + +fn is_hex_encoded_eip712_scalar(field_type: &str) -> bool { + field_type.starts_with("bytes") + || field_type.starts_with("uint") + || field_type.starts_with("int") +} + +fn normalize_bankr_numeric_fields(value: &mut Value) { + let Some(types) = value.get("types").and_then(Value::as_object).cloned() else { + return; + }; + if let Some(domain) = value.get_mut("domain") { + normalize_struct_bankr_numeric_fields("EIP712Domain", domain, &types); + } + let Some(primary_type) = value + .get("primaryType") + .and_then(Value::as_str) + .map(str::to_string) + else { + return; + }; + if let Some(message) = value.get_mut("message") { + normalize_struct_bankr_numeric_fields(&primary_type, message, &types); + } +} + +fn normalize_struct_bankr_numeric_fields( + type_name: &str, + value: &mut Value, + types: &serde_json::Map, +) { + let Some(fields) = types.get(type_name).and_then(Value::as_array).cloned() else { + return; + }; + let Some(object) = value.as_object_mut() else { + return; + }; + + for field in fields { + let Some(name) = field.get("name").and_then(Value::as_str) else { + continue; + }; + let Some(field_type) = field.get("type").and_then(Value::as_str) else { + continue; + }; + let Some(field_value) = object.get_mut(name) else { + continue; + }; + normalize_field_bankr_numeric_values(field_type, field_value, types); + } +} + +fn normalize_field_bankr_numeric_values( + field_type: &str, + value: &mut Value, + types: &serde_json::Map, +) { + if let Some(element_type) = field_type.strip_suffix("[]") { + if let Some(values) = value.as_array_mut() { + for value in values { + normalize_field_bankr_numeric_values(element_type, value, types); + } + } + return; + } + + if types.contains_key(field_type) { + normalize_struct_bankr_numeric_fields(field_type, value, types); + return; + } + + if is_eip712_integer_scalar(field_type) { + normalize_bankr_integer_scalar(value); + } +} + +fn is_eip712_integer_scalar(field_type: &str) -> bool { + field_type.starts_with("uint") || field_type.starts_with("int") +} + +fn normalize_bankr_integer_scalar(value: &mut Value) { + let Some(text) = value.as_str() else { + return; + }; + if let Some(number) = parse_bankr_integer_scalar(text) { + *value = serde_json::Number::from(number).into(); + } +} + +fn parse_bankr_integer_scalar(text: &str) -> Option { + let trimmed = text.trim(); + if trimmed.is_empty() { + return None; + } + if let Some(hex) = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16).ok(); + } + trimmed.parse::().ok() +} + +fn hyperliquid_signature_from_bankr( + response: SignResponse, +) -> Result { + let signature = canonical_signature_hex(&response.signature)?; + signature.parse::().map_err(|err| { + CliError::InvalidAuth(format!( + "bankr_malformed_response: failed to parse Bankr signature: {err}" + )) + }) +} + +fn alloy_signature_from_bankr( + response: SignResponse, +) -> Result { + let signature = hyperliquid_signature_from_bankr(response)?; + Ok(signature.into()) +} + +fn canonical_signature_hex(signature: &str) -> Result { + let sig_hex = signature.strip_prefix("0x").unwrap_or(signature); + if sig_hex.len() != 130 { + return Err(CliError::InvalidAuth(format!( + "bankr_malformed_response: unexpected Bankr signature length {} hex chars; expected 130", + sig_hex.len() + ))); + } + let v = u8::from_str_radix(&sig_hex[128..130], 16).map_err(|err| { + CliError::InvalidAuth(format!( + "bankr_malformed_response: invalid Bankr recovery byte: {err}" + )) + })?; + let canonical_v = if v >= 27 { v } else { v + 27 }; + Ok(format!("0x{}{canonical_v:02x}", &sig_hex[..128])) +} + +fn verify_recovered_address( + config: &BankrSigningConfig, + signature: &alloy_primitives::Signature, + signing_hash: B256, +) -> Result<(), CliError> { + let recovered = signature + .recover_address_from_prehash(&signing_hash) + .map_err(|err| { + CliError::InvalidAuth(format!( + "bankr_malformed_response: failed to recover Bankr signer: {err}" + )) + })?; + verify_address_matches(config, recovered) +} + +fn verify_address_matches( + config: &BankrSigningConfig, + recovered: AlloyAddress, +) -> Result<(), CliError> { + let expected: AlloyAddress = config.address; + if recovered == expected { + return Ok(()); + } + + Err(CliError::InvalidAuth(format!( + "bankr_signer_mismatch: Bankr signature recovered {recovered}, expected {}", + config.address + ))) +} + +#[derive(Debug, Deserialize)] +struct WalletMeResponse { + #[serde(default, rename = "evmAddress")] + evm_address: Option, + #[serde(default)] + wallets: Vec, +} + +impl WalletMeResponse { + fn evm_address(&self) -> Option<&str> { + self.evm_address.as_deref().or_else(|| { + self.wallets + .iter() + .find(|wallet| wallet.chain.eq_ignore_ascii_case("evm")) + .map(|wallet| wallet.address.as_str()) + }) + } +} + +#[derive(Debug, Deserialize)] +struct BankrWalletAddress { + chain: String, + address: String, +} + +#[derive(Debug, Deserialize)] +struct SignResponse { + signature: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + use alloy_v1::signers::SignerSync; + use hypersdk::hypercore::PrivateKeySigner; + use wiremock::matchers::{header, method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + const KEY: &str = "0x0000000000000000000000000000000000000000000000000000000000000009"; + const API_KEY: &str = "bk_test_key"; + + fn test_typed_data() -> TypedData { + serde_json::from_value(serde_json::json!({ + "types": { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "chainId", "type": "uint256"} + ], + "TestAction": [ + {"name": "message", "type": "string"} + ] + }, + "primaryType": "TestAction", + "domain": {"name": "Hyperliquid", "chainId": "0x3e7"}, + "message": {"message": "hello bankr"} + })) + .unwrap() + } + + fn config(api_base_url: String) -> BankrSigningConfig { + let signer = KEY.parse::().unwrap(); + BankrSigningConfig::new( + "default".to_string(), + API_KEY.to_string(), + api_base_url, + signer.address(), + ) + } + + #[test] + fn bankr_typed_data_value_normalizes_integer_fields_for_bankr_api() { + let typed_data = test_typed_data(); + let value = bankr_typed_data_value(&typed_data).unwrap(); + + assert_eq!(value["domain"]["chainId"], 999); + } + + async fn run_bankr(f: impl FnOnce() -> T + Send + 'static) -> T { + tokio::task::spawn_blocking(f).await.unwrap() + } + + #[tokio::test(flavor = "multi_thread")] + async fn wallet_address_extracts_evm_wallet_from_bankr_response() { + let server = MockServer::start().await; + let signer = KEY.parse::().unwrap(); + Mock::given(method("GET")) + .and(path("/wallet/me")) + .and(header("X-API-Key", API_KEY)) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "success": true, + "wallets": [ + {"chain": "solana", "address": "5DcK"}, + {"chain": "evm", "address": signer.address().to_string()} + ] + }))) + .mount(&server) + .await; + + let server_uri = server.uri(); + let address = run_bankr(move || wallet_address(&server_uri, API_KEY).unwrap()).await; + + assert_eq!(address, signer.address()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn sign_typed_data_verifies_recovered_bankr_address() { + let server = MockServer::start().await; + let typed_data = test_typed_data(); + let signer = KEY.parse::().unwrap(); + let signature = signer.sign_dynamic_typed_data_sync(&typed_data).unwrap(); + Mock::given(method("POST")) + .and(path("/wallet/sign")) + .and(header("X-API-Key", API_KEY)) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "success": true, + "signature": signature.to_string(), + "signer": signer.address().to_string(), + "signatureType": "eth_signTypedData_v4" + }))) + .mount(&server) + .await; + + let server_uri = server.uri(); + let typed_data_for_signing = typed_data.clone(); + let signature = run_bankr(move || { + sign_typed_data(&config(server_uri), &typed_data_for_signing).unwrap() + }) + .await; + let alloy_signature: alloy_primitives::Signature = signature.into(); + let recovered = alloy_signature + .recover_address_from_prehash(&typed_data.eip712_signing_hash().unwrap()) + .unwrap(); + + assert_eq!(recovered, signer.address()); + let requests = server.received_requests().await.unwrap(); + assert_eq!( + requests[0].body_json::().unwrap()["signatureType"], + "eth_signTypedData_v4" + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn sign_typed_data_rejects_signer_mismatch() { + let server = MockServer::start().await; + let typed_data = test_typed_data(); + let signer = KEY.parse::().unwrap(); + let other = "0x000000000000000000000000000000000000000000000000000000000000000a" + .parse::() + .unwrap(); + let signature = other.sign_dynamic_typed_data_sync(&typed_data).unwrap(); + Mock::given(method("POST")) + .and(path("/wallet/sign")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "success": true, + "signature": signature.to_string() + }))) + .mount(&server) + .await; + + let server_uri = server.uri(); + let typed_data_for_signing = typed_data.clone(); + let err = run_bankr(move || { + sign_typed_data(&config(server_uri), &typed_data_for_signing).unwrap_err() + }) + .await; + + assert_eq!(err.exit_code(), 10); + assert!(err.to_string().contains("bankr_signer_mismatch")); + assert_ne!(signer.address(), other.address()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn sign_message_verifies_recovered_bankr_address() { + let server = MockServer::start().await; + let signer = KEY.parse::().unwrap(); + let signature = signer.sign_message_sync(b"hello bankr").unwrap(); + Mock::given(method("POST")) + .and(path("/wallet/sign")) + .and(header("X-API-Key", API_KEY)) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "success": true, + "signature": signature.to_string(), + "signer": signer.address().to_string(), + "signatureType": "personal_sign" + }))) + .mount(&server) + .await; + + let server_uri = server.uri(); + let signature = + run_bankr(move || sign_message(&config(server_uri), b"hello bankr").unwrap()).await; + let recovered = signature.recover_address_from_msg(b"hello bankr").unwrap(); + + assert_eq!(recovered, signer.address()); + let requests = server.received_requests().await.unwrap(); + assert_eq!( + requests[0].body_json::().unwrap()["signatureType"], + "personal_sign" + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn sign_message_rejects_non_utf8_messages_before_request() { + let server = MockServer::start().await; + let err = sign_message(&config(server.uri()), &[0xff]).unwrap_err(); + + assert_eq!(err.exit_code(), 13); + assert!(err.to_string().contains("bankr_unsupported_message")); + assert!(server.received_requests().await.unwrap().is_empty()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn bankr_forbidden_response_maps_to_auth_failure_without_leaking_key() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/wallet/me")) + .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({ + "error": "Read-only API key" + }))) + .mount(&server) + .await; + + let server_uri = server.uri(); + let err = run_bankr(move || wallet_address(&server_uri, API_KEY).unwrap_err()).await; + + assert_eq!(err.exit_code(), 10); + assert!(err.to_string().contains("bankr_access_denied")); + assert!(!err.to_string().contains(API_KEY)); + } +} diff --git a/src/cli_runtime.rs b/src/cli_runtime.rs index e64720f..bec9c1d 100644 --- a/src/cli_runtime.rs +++ b/src/cli_runtime.rs @@ -67,6 +67,7 @@ async fn run_command(cli: &Cli, registry_path: Option<&[String]>) -> Result<(), context.keystore_password.as_deref(), context.account.as_deref(), context.ows_signer.as_deref(), + context.bankr_signer.as_deref(), cli.format, ), Some(Commands::Wallet { @@ -77,6 +78,7 @@ async fn run_command(cli: &Cli, registry_path: Option<&[String]>) -> Result<(), context.keystore_password.as_deref(), context.account.as_deref(), context.ows_signer.as_deref(), + context.bankr_signer.as_deref(), cli.format, ), Some(Commands::Perps { @@ -1440,6 +1442,7 @@ struct AppContext { keystore_password: Option, account: Option, ows_signer: Option, + bankr_signer: Option, network: config::Network, api_base_url_override: Option, } @@ -1452,17 +1455,19 @@ impl AppContext { } else { config::Network::Mainnet }; - let private_key = if cli.account.is_some() || cli.ows_signer.is_some() { - None - } else { - config::resolve_private_key(cli.private_key.as_deref())? - }; + let private_key = + if cli.account.is_some() || cli.ows_signer.is_some() || cli.bankr_signer.is_some() { + None + } else { + config::resolve_private_key(cli.private_key.as_deref())? + }; Ok(Self { private_key, keystore: cli.keystore.clone(), keystore_password: cli.keystore_password.clone(), account: cli.account.clone(), ows_signer: cli.ows_signer.clone(), + bankr_signer: cli.bankr_signer.clone(), network, api_base_url_override: config::resolve_api_base_url_override_for_network(network)?, }) @@ -1500,6 +1505,7 @@ impl AppContext { keystore_password: self.keystore_password.as_deref(), account_selector: self.account.as_deref(), ows_selector: self.ows_signer.as_deref(), + bankr_selector: self.bankr_signer.as_deref(), default_fallback: DefaultSignerFallback::AllowStoredDefaultOrFirst, }) } diff --git a/src/command_catalog.json b/src/command_catalog.json index beaa4b2..d7c92bc 100644 --- a/src/command_catalog.json +++ b/src/command_catalog.json @@ -14,7 +14,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid meta", @@ -28,7 +29,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid perps list", @@ -49,7 +51,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid perps get ", @@ -77,7 +80,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid spot list", @@ -91,7 +95,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid spot get ", @@ -113,7 +118,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid book ", @@ -148,7 +154,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid mids", @@ -176,7 +183,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid candles ", @@ -224,7 +232,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid spread ", @@ -246,7 +255,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid funding ", @@ -268,7 +278,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid asset decode ", @@ -291,7 +302,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid asset search ", @@ -320,7 +332,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid outcomes list", @@ -342,7 +355,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid outcomes get ", @@ -363,7 +377,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid builder max-fee", @@ -392,7 +407,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid builder approved", @@ -414,7 +430,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid builder approve", @@ -451,7 +468,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid account fills
", @@ -490,7 +508,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account fees
", @@ -512,7 +531,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account rate-limit
", @@ -534,7 +554,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account orders
", @@ -556,7 +577,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account portfolio
", @@ -578,7 +600,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account subaccounts
", @@ -600,7 +623,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account portfolio-history
", @@ -622,7 +646,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account ledger
", @@ -655,7 +680,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account funding
", @@ -688,7 +714,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account twap-history
", @@ -710,7 +737,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account twap-fills
", @@ -749,7 +777,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account abstraction
", @@ -771,7 +800,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid account abstraction set", @@ -805,7 +835,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid subaccount list
", @@ -830,7 +861,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid subaccount create", @@ -851,7 +883,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid subaccount transfer", @@ -898,7 +931,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid subaccount spot-transfer", @@ -952,7 +986,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid account ls", @@ -966,7 +1001,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid account add ", @@ -1003,7 +1039,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid account set-default ", @@ -1024,7 +1061,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid account remove ", @@ -1053,7 +1091,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid api-wallet create", @@ -1091,7 +1130,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid api-wallet approve", @@ -1129,7 +1169,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid api-wallet list", @@ -1153,7 +1194,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid api-wallet revoke", @@ -1174,7 +1216,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid wallet create", @@ -1188,7 +1231,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid wallet import ", @@ -1208,7 +1252,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid wallet show", @@ -1222,7 +1267,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "address_selector_supported" + "ows_signer": "address_selector_supported", + "bankr_signer": "address_selector_supported" }, { "command": "hyperliquid wallet address", @@ -1236,7 +1282,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "address_selector_supported" + "ows_signer": "address_selector_supported", + "bankr_signer": "address_selector_supported" }, { "command": "hyperliquid wallet reset", @@ -1259,7 +1306,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid wallet import-mnemonic ", @@ -1284,7 +1332,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid wallet list", @@ -1298,7 +1347,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid wallet rename ", @@ -1325,7 +1375,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid wallet delete ", @@ -1354,7 +1405,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid wallet export ", @@ -1383,7 +1435,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" }, { "command": "hyperliquid orders open", @@ -1411,7 +1464,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders history", @@ -1425,7 +1479,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders status", @@ -1468,7 +1523,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid positions list", @@ -1496,7 +1552,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking validators", @@ -1510,7 +1567,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid staking summary
", @@ -1532,7 +1590,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid staking rewards
", @@ -1554,7 +1613,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid staking history
", @@ -1576,7 +1636,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid vault list", @@ -1633,7 +1694,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid vault search ", @@ -1679,7 +1741,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid vault get
", @@ -1701,7 +1764,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid vault positions
", @@ -1723,7 +1787,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid borrowlend rates", @@ -1737,7 +1802,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid borrowlend get ", @@ -1759,7 +1825,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid borrowlend user
", @@ -1781,7 +1848,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid borrowlend supply --amount ", @@ -1810,7 +1878,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid borrowlend withdraw --amount ", @@ -1843,7 +1912,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid prio status", @@ -1857,7 +1927,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid referral status", @@ -1871,7 +1942,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders create", @@ -2032,7 +2104,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders scale", @@ -2137,7 +2210,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders batch-create", @@ -2173,7 +2247,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders tpsl", @@ -2260,7 +2335,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders cancel ", @@ -2293,7 +2369,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders cancel-all", @@ -2334,7 +2411,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders modify ", @@ -2394,7 +2472,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders twap-create", @@ -2470,7 +2549,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders twap-cancel ", @@ -2512,7 +2592,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid positions update-leverage", @@ -2548,7 +2629,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid positions update-margin", @@ -2577,7 +2659,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid orders schedule-cancel", @@ -2620,7 +2703,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid transfer spot-to-perp", @@ -2650,7 +2734,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid transfer perp-to-spot", @@ -2680,7 +2765,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid transfer send", @@ -2720,7 +2806,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid transfer spot-send", @@ -2764,7 +2851,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid transfer send-asset", @@ -2827,7 +2915,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid transfer withdraw", @@ -2864,7 +2953,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking delegate", @@ -2893,7 +2983,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking undelegate", @@ -2922,7 +3013,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking deposit", @@ -2944,7 +3036,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking withdraw", @@ -2966,7 +3059,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking claim-rewards", @@ -2980,7 +3074,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking link initiate", @@ -3010,7 +3105,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid staking link finalize", @@ -3040,7 +3136,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "prompt", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid vault deposit", @@ -3069,7 +3166,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid vault withdraw", @@ -3098,7 +3196,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid prio bid", @@ -3132,7 +3231,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "local_only" + "ows_signer": "local_only", + "bankr_signer": "local_only" }, { "command": "hyperliquid referral set ", @@ -3152,7 +3252,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid referral register ", @@ -3173,7 +3274,8 @@ "dry_run": "optional", "raw_payload": "dry_run_only", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid subscribe trades", @@ -3206,7 +3308,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid subscribe orderbook", @@ -3239,7 +3342,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid subscribe candles", @@ -3278,7 +3382,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid subscribe all-mids", @@ -3305,7 +3410,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid subscribe order-updates", @@ -3332,7 +3438,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid subscribe fills", @@ -3359,7 +3466,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "experimental_feature_gated" + "ows_signer": "experimental_feature_gated", + "bankr_signer": "unsupported_raw_l1_signing" }, { "command": "hyperliquid feedback", @@ -3413,7 +3521,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid schema", @@ -3434,7 +3543,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid update", @@ -3448,7 +3558,8 @@ "dry_run": "optional", "raw_payload": "unsupported", "confirmation": "none", - "ows_signer": "not_required" + "ows_signer": "not_required", + "bankr_signer": "not_required" }, { "command": "hyperliquid setup", @@ -3485,7 +3596,8 @@ "dry_run": "not_supported", "raw_payload": "unsupported", "confirmation": "prompt", - "ows_signer": "not_applicable" + "ows_signer": "not_applicable", + "bankr_signer": "not_applicable" } ] } diff --git a/src/command_metadata.rs b/src/command_metadata.rs index 6283844..08c1d7a 100644 --- a/src/command_metadata.rs +++ b/src/command_metadata.rs @@ -8,6 +8,7 @@ pub(crate) trait CatalogCommandMetadata { fn raw_payload(&self) -> Option<&str>; fn confirmation(&self) -> Option<&str>; fn ows_signer(&self) -> Option<&str>; + fn bankr_signer(&self) -> Option<&str>; fn auth_required(&self) -> bool; fn dangerous(&self) -> bool; } @@ -30,6 +31,7 @@ pub(crate) struct CommandMetadata { pub(crate) raw_payload: String, pub(crate) confirmation: String, pub(crate) ows_signer: String, + pub(crate) bankr_signer: String, } pub(crate) fn command_metadata( @@ -66,6 +68,10 @@ pub(crate) fn command_metadata( .ows_signer() .map(str::to_string) .unwrap_or_else(|| inferred_ows_signer(command_key, command)), + bankr_signer: command + .bankr_signer() + .map(str::to_string) + .unwrap_or_else(|| inferred_bankr_signer(command_key, command)), } } @@ -177,6 +183,22 @@ fn inferred_ows_signer(command_key: &str, command: &impl CatalogCommandMetadata) "not_required".to_string() } +fn inferred_bankr_signer(command_key: &str, command: &impl CatalogCommandMetadata) -> String { + if matches!(command_key, "wallet show" | "wallet address") { + return "address_selector_supported".to_string(); + } + if command_key == "prio bid" { + return "local_only".to_string(); + } + if is_local_only_command(command_key) { + return "not_applicable".to_string(); + } + if command.auth_required() { + return "unsupported_raw_l1_signing".to_string(); + } + "not_required".to_string() +} + fn is_local_only_command(command_key: &str) -> bool { is_interactive_local_command(command_key) || is_local_state_command(command_key) diff --git a/src/command_registry.rs b/src/command_registry.rs index 30050ae..fd3765f 100644 --- a/src/command_registry.rs +++ b/src/command_registry.rs @@ -70,6 +70,7 @@ pub struct CommandContract { pub confirmation: ConfirmationPolicy, pub transport: Vec, pub ows_signer: OwsSupport, + pub bankr_signer: BankrSupport, pub output_contract: OutputContract, pub handler: HandlerBinding, pub one_of_required: Vec>, @@ -92,6 +93,7 @@ impl CommandContract { let raw_payload = RawPayloadPolicy::from(metadata.raw_payload.as_str()); let confirmation = ConfirmationPolicy::from(metadata.confirmation.as_str()); let ows_signer = OwsSupport::from(metadata.ows_signer.as_str()); + let bankr_signer = BankrSupport::from(metadata.bankr_signer.as_str()); let transport = primary_transport(lifecycle); Ok(Self { command: command.command, @@ -109,6 +111,7 @@ impl CommandContract { confirmation, transport, ows_signer, + bankr_signer, output_contract: output_contract(&command_key, lifecycle, dry_run), handler: HandlerBinding::for_command_key(&command_key), one_of_required: command.one_of_required, @@ -157,6 +160,7 @@ impl CommandContract { "arg": if yes_support { Some("yes") } else { None }, }, "ows_signer": self.ows_signer, + "bankr_signer": self.bankr_signer, "transport": self.transport, "output_contract": self.output_contract, "stream_bounds": stream_bounds, @@ -266,6 +270,17 @@ pub enum OwsSupport { NotApplicable, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum BankrSupport { + NotRequired, + AddressSelectorSupported, + TypedDataSupported, + UnsupportedRawL1Signing, + LocalOnly, + NotApplicable, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum OutputContract { @@ -397,6 +412,19 @@ impl From<&str> for OwsSupport { } } +impl From<&str> for BankrSupport { + fn from(value: &str) -> Self { + match value { + "address_selector_supported" => Self::AddressSelectorSupported, + "typed_data_supported" => Self::TypedDataSupported, + "unsupported_raw_l1_signing" => Self::UnsupportedRawL1Signing, + "local_only" => Self::LocalOnly, + "not_applicable" => Self::NotApplicable, + _ => Self::NotRequired, + } + } +} + impl From<&str> for InputKind { fn from(value: &str) -> Self { match value { @@ -488,6 +516,8 @@ struct CatalogCommand { confirmation: Option, #[serde(default)] ows_signer: Option, + #[serde(default)] + bankr_signer: Option, description: String, #[serde(default)] one_of_required: Vec>, @@ -553,6 +583,10 @@ impl CatalogCommandMetadata for CatalogCommand { self.ows_signer.as_deref() } + fn bankr_signer(&self) -> Option<&str> { + self.bankr_signer.as_deref() + } + fn auth_required(&self) -> bool { self.auth_required } diff --git a/src/commands/schema.rs b/src/commands/schema.rs index 1b4e785..6c529e0 100644 --- a/src/commands/schema.rs +++ b/src/commands/schema.rs @@ -39,6 +39,7 @@ struct CommandSchema { confirmation: String, confirmation_bypass: Value, ows_signer: String, + bankr_signer: String, transport: Value, output_contract: String, stream_bounds: Value, @@ -172,6 +173,7 @@ fn command_schema(command: &CommandContract) -> CommandSchema { .cloned() .unwrap_or_else(|| json!({"supported": false, "arg": null})), ows_signer: metadata_string(&metadata, "ows_signer"), + bankr_signer: metadata_string(&metadata, "bankr_signer"), transport: metadata .get("transport") .cloned() diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs index 91bb529..062fb01 100644 --- a/src/commands/wallet.rs +++ b/src/commands/wallet.rs @@ -406,8 +406,23 @@ pub fn show( keystore_password: Option<&str>, account_selector: Option<&str>, ows_selector: Option<&str>, + bankr_selector: Option<&str>, format: OutputFormat, ) -> Result<(), anyhow::Error> { + if let Some(selector) = bankr_selector { + let resolved = auth::resolve_bankr_signer(selector)?; + let (alias, source) = signer_display(resolved.source()); + print_wallet_info( + "Current wallet", + &resolved.address().to_string(), + alias, + &source, + None, + format, + ); + return Ok(()); + } + // When --ows-signer is explicit, resolve the specific wallet. // When no signer at all is specified, auto-detect the first OWS wallet. let vault_path = crate::ows::ows_vault_path(); @@ -511,6 +526,7 @@ fn signer_display(source: &SignerSource) -> (Option, String) { (Some(alias.clone()), "stored signing account".to_string()) } SignerSource::Ows { selector } => (None, format!("OWS signer ({selector})")), + SignerSource::Bankr { selector } => (None, format!("Bankr signer ({selector})")), } } @@ -520,11 +536,14 @@ pub fn address( keystore_password: Option<&str>, account_selector: Option<&str>, ows_selector: Option<&str>, + bankr_selector: Option<&str>, format: OutputFormat, ) -> Result<(), anyhow::Error> { let vault_path = crate::ows::ows_vault_path(); - let address_str = if let Some(selector) = ows_selector { + let address_str = if let Some(selector) = bankr_selector { + auth::resolve_bankr_signer(selector)?.address().to_string() + } else if let Some(selector) = ows_selector { // Explicit --ows-signer: resolve the specific wallet match crate::ows::get_ows_wallet(selector, vault_path.as_deref()) { Ok(wallet) => { diff --git a/src/lib.rs b/src/lib.rs index 47e40ae..afe1841 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ extern crate alloy_signer_local_v1 as _; pub mod auth; +pub mod bankr; pub mod command_context; pub mod command_handlers; pub(crate) mod command_metadata; diff --git a/src/main.rs b/src/main.rs index 17ae703..2ad7327 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,7 @@ struct Cli { #[arg( long, global = true, - conflicts_with_all = ["private_key", "keystore", "keystore_password", "ows_signer"] + conflicts_with_all = ["private_key", "keystore", "keystore_password", "ows_signer", "bankr_signer"] )] account: Option, @@ -75,10 +75,19 @@ struct Cli { global = true, value_name = "SELECTOR", alias = "wallet", - conflicts_with_all = ["private_key", "keystore", "keystore_password", "account"] + conflicts_with_all = ["private_key", "keystore", "keystore_password", "account", "bankr_signer"] )] ows_signer: Option, + /// Bankr signer selector. Currently only 'default' is supported and uses BANKR_API_KEY. + #[arg( + long, + global = true, + value_name = "SELECTOR", + conflicts_with_all = ["private_key", "keystore", "keystore_password", "account", "ows_signer"] + )] + bankr_signer: Option, + /// Use testnet instead of mainnet #[arg(long, global = true)] testnet: bool, diff --git a/src/resolvers.rs b/src/resolvers.rs index d131169..4695903 100644 --- a/src/resolvers.rs +++ b/src/resolvers.rs @@ -27,6 +27,7 @@ pub struct SignerResolverInput<'a> { pub keystore_password: Option<&'a str>, pub account_selector: Option<&'a str>, pub ows_selector: Option<&'a str>, + pub bankr_selector: Option<&'a str>, pub default_fallback: DefaultSignerFallback, } @@ -37,6 +38,10 @@ pub fn resolve_selected_signer( return auth::resolve_signer_with_account_and_ows(None, None, None, None, Some(selector)); } + if let Some(selector) = input.bankr_selector { + return auth::resolve_bankr_signer(selector); + } + if let Some(private_key) = input.resolved_private_key { return auth::resolve_signer_with_account_and_ows( Some(private_key), @@ -276,6 +281,7 @@ mod tests { keystore_password: None, account_selector: None, ows_selector: Some("0x0000000000000000000000000000000000000001"), + bankr_selector: None, default_fallback: DefaultSignerFallback::Disallow, }) .unwrap(); diff --git a/src/signing.rs b/src/signing.rs index fecbfc2..b3cc031 100644 --- a/src/signing.rs +++ b/src/signing.rs @@ -25,12 +25,14 @@ pub enum SignerSource { Keystore, StoredAccount { alias: String }, Ows { selector: String }, + Bankr { selector: String }, } #[derive(Debug, Clone)] enum SignerBackend { LocalPrivateKey(PrivateKeySigner), Ows(crate::ows::OwsSigningConfig), + Bankr(crate::bankr::BankrSigningConfig), } #[derive(Debug, Clone)] @@ -70,11 +72,23 @@ impl SelectedSigner { } } + #[must_use] + pub fn bankr(config: crate::bankr::BankrSigningConfig) -> Self { + let selector = config.selector().to_string(); + let address = config.address(); + Self { + backend: SignerBackend::Bankr(config), + source: SignerSource::Bankr { selector }, + query_address: address, + } + } + #[must_use] pub fn address(&self) -> Address { match &self.backend { SignerBackend::LocalPrivateKey(signer) => signer.address(), SignerBackend::Ows(config) => config.address(), + SignerBackend::Bankr(config) => config.address(), } } @@ -94,6 +108,9 @@ impl SelectedSigner { SignerBackend::Ows(config) => { Err(crate::ows::unsupported_live_signing(config.selector())) } + SignerBackend::Bankr(config) => Err(crate::bankr::unsupported_local_private_key( + config.selector(), + )), } } @@ -108,6 +125,7 @@ impl SelectedSigner { SignerBackend::Ows(config) => { Err(crate::ows::unsupported_live_signing(config.selector())) } + SignerBackend::Bankr(_) => Ok(()), } } @@ -148,6 +166,10 @@ impl SelectedSigner { CliError::Internal(anyhow::anyhow!("failed to encode signed action: {err}")) }) } + SignerBackend::Bankr(config) => { + let _ = (action, nonce, vault_address, chain); + Err(crate::bankr::unsupported_raw_l1_signing(config.selector())) + } } } @@ -158,6 +180,7 @@ impl SelectedSigner { .map(Into::into) .map_err(|err| CliError::Internal(anyhow::anyhow!("failed to sign action: {err}"))), SignerBackend::Ows(config) => crate::ows::sign_typed_data(config, typed_data), + SignerBackend::Bankr(config) => crate::bankr::sign_typed_data(config, typed_data), } } @@ -173,6 +196,10 @@ impl SelectedSigner { SignerBackend::Ows(config) => { crate::ows::sign_hash(config, agent_signing_hash(chain, connection_id)) } + SignerBackend::Bankr(config) => { + let _ = (chain, connection_id); + Err(crate::bankr::unsupported_raw_l1_signing(config.selector())) + } } } @@ -184,6 +211,7 @@ impl SelectedSigner { }) } SignerBackend::Ows(config) => crate::ows::sign_message(config, message), + SignerBackend::Bankr(config) => crate::bankr::sign_message(config, message), } } } @@ -279,4 +307,36 @@ mod tests { ); assert_eq!(selected.sign_message(b"nope").unwrap_err().exit_code(), 13); } + + #[test] + fn bankr_signer_rejects_raw_l1_action_signing() { + let address: Address = "0x0000000000000000000000000000000000000001" + .parse() + .unwrap(); + let selected = SelectedSigner::bankr(crate::bankr::BankrSigningConfig::new( + "default".to_string(), + "bk_test_key".to_string(), + "https://api.bankr.bot".to_string(), + address, + )); + + let err = selected + .sign_l1_action_sync( + Action::UpdateLeverage(hypersdk::hypercore::api::UpdateLeverage { + asset: 3, + is_cross: true, + leverage: 2, + }), + 1_777_963_000_000, + None, + Chain::Testnet, + ) + .unwrap_err(); + + assert_eq!(err.exit_code(), 13); + assert!( + err.to_string() + .contains("cannot sign raw Hyperliquid L1 action hashes") + ); + } } diff --git a/tests/fixtures/contracts/registry_inventory.json b/tests/fixtures/contracts/registry_inventory.json index 78d3771..51cae3e 100644 --- a/tests/fixtures/contracts/registry_inventory.json +++ b/tests/fixtures/contracts/registry_inventory.json @@ -4,6 +4,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid status", "command_path": [ "status" @@ -43,6 +44,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid meta", "command_path": [ "meta" @@ -82,6 +84,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid perps list", "command_path": [ "perps", @@ -136,6 +139,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid perps get ", "command_path": [ "perps", @@ -203,6 +207,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid spot list", "command_path": [ "spot", @@ -243,6 +248,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid spot get ", "command_path": [ "spot", @@ -297,6 +303,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid book ", "command_path": [ "book" @@ -376,6 +383,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid mids", "command_path": [ "mids" @@ -442,6 +450,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid candles ", "command_path": [ "candles" @@ -547,6 +556,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid spread ", "command_path": [ "spread" @@ -600,6 +610,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid funding ", "command_path": [ "funding" @@ -653,6 +664,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid asset decode ", "command_path": [ "asset", @@ -707,6 +719,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid asset search ", "command_path": [ "asset", @@ -774,6 +787,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid outcomes list", "command_path": [ "outcomes", @@ -828,6 +842,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid outcomes get ", "command_path": [ "outcomes", @@ -882,6 +897,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid builder max-fee", "command_path": [ "builder", @@ -949,6 +965,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid builder approved", "command_path": [ "builder", @@ -1003,6 +1020,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid builder approve", "command_path": [ "builder", @@ -1083,6 +1101,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account fills
", "command_path": [ "account", @@ -1176,6 +1195,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account fees
", "command_path": [ "account", @@ -1230,6 +1250,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account rate-limit
", "command_path": [ "account", @@ -1284,6 +1305,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account orders
", "command_path": [ "account", @@ -1338,6 +1360,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account portfolio
", "command_path": [ "account", @@ -1392,6 +1415,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account subaccounts
", "command_path": [ "account", @@ -1446,6 +1470,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account portfolio-history
", "command_path": [ "account", @@ -1500,6 +1525,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account ledger
", "command_path": [ "account", @@ -1580,6 +1606,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account funding
", "command_path": [ "account", @@ -1660,6 +1687,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account twap-history
", "command_path": [ "account", @@ -1714,6 +1742,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account twap-fills
", "command_path": [ "account", @@ -1807,6 +1836,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid account abstraction
", "command_path": [ "account", @@ -1861,6 +1891,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid account abstraction set", "command_path": [ "account", @@ -1935,6 +1966,7 @@ "subaccounts" ], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid subaccount list
", "command_path": [ "subaccount", @@ -1991,6 +2023,7 @@ "subaccounts" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subaccount create", "command_path": [ "subaccount", @@ -2047,6 +2080,7 @@ "subaccounts" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subaccount transfer", "command_path": [ "subaccount", @@ -2145,6 +2179,7 @@ "subaccounts" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subaccount spot-transfer", "command_path": [ "subaccount", @@ -2254,6 +2289,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account ls", "command_path": [ "account", @@ -2294,6 +2330,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account add ", "command_path": [ "account", @@ -2387,6 +2424,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account set-default ", "command_path": [ "account", @@ -2441,6 +2479,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account remove ", "command_path": [ "account", @@ -2510,6 +2549,7 @@ "api-wallets" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid api-wallet create", "command_path": [ "api-wallet", @@ -2605,6 +2645,7 @@ "api-wallets" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid api-wallet approve", "command_path": [ "api-wallet", @@ -2700,6 +2741,7 @@ "api-wallets" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid api-wallet list", "command_path": [ "api-wallet", @@ -2756,6 +2798,7 @@ "api-wallets" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid api-wallet revoke", "command_path": [ "api-wallet", @@ -2810,6 +2853,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet create", "command_path": [ "wallet", @@ -2850,6 +2894,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet import ", "command_path": [ "wallet", @@ -2904,6 +2949,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "address_selector_supported", "command": "hyperliquid wallet show", "command_path": [ "wallet", @@ -2944,6 +2990,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "address_selector_supported", "command": "hyperliquid wallet address", "command_path": [ "wallet", @@ -2984,6 +3031,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet reset", "command_path": [ "wallet", @@ -3038,6 +3086,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet import-mnemonic ", "command_path": [ "wallet", @@ -3105,6 +3154,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet list", "command_path": [ "wallet", @@ -3145,6 +3195,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet rename ", "command_path": [ "wallet", @@ -3212,6 +3263,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet delete ", "command_path": [ "wallet", @@ -3279,6 +3331,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet export ", "command_path": [ "wallet", @@ -3346,6 +3399,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders open", "command_path": [ "orders", @@ -3413,6 +3467,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders history", "command_path": [ "orders", @@ -3453,6 +3508,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid orders status", "command_path": [ "orders", @@ -3540,6 +3596,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid positions list", "command_path": [ "positions", @@ -3607,6 +3664,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid staking validators", "command_path": [ "staking", @@ -3647,6 +3705,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid staking summary
", "command_path": [ "staking", @@ -3701,6 +3760,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid staking rewards
", "command_path": [ "staking", @@ -3755,6 +3815,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid staking history
", "command_path": [ "staking", @@ -3811,6 +3872,7 @@ "vaults" ], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid vault list", "command_path": [ "vault", @@ -3918,6 +3980,7 @@ "vaults" ], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid vault search ", "command_path": [ "vault", @@ -4018,6 +4081,7 @@ "vaults" ], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid vault get
", "command_path": [ "vault", @@ -4074,6 +4138,7 @@ "vaults" ], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid vault positions
", "command_path": [ "vault", @@ -4128,6 +4193,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid borrowlend rates", "command_path": [ "borrowlend", @@ -4168,6 +4234,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid borrowlend get ", "command_path": [ "borrowlend", @@ -4222,6 +4289,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid borrowlend user
", "command_path": [ "borrowlend", @@ -4276,6 +4344,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid borrowlend supply --amount ", "command_path": [ "borrowlend", @@ -4343,6 +4412,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid borrowlend withdraw --amount ", "command_path": [ "borrowlend", @@ -4423,6 +4493,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid prio status", "command_path": [ "prio", @@ -4463,6 +4534,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid referral status", "command_path": [ "referral", @@ -4503,6 +4575,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders create", "command_path": [ "orders", @@ -4823,6 +4896,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders scale", "command_path": [ "orders", @@ -5030,6 +5104,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders batch-create", "command_path": [ "orders", @@ -5110,6 +5185,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders tpsl", "command_path": [ "orders", @@ -5289,6 +5365,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders cancel ", "command_path": [ "orders", @@ -5369,6 +5446,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders cancel-all", "command_path": [ "orders", @@ -5462,6 +5540,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders modify ", "command_path": [ "orders", @@ -5588,6 +5667,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders twap-create", "command_path": [ "orders", @@ -5739,6 +5819,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders twap-cancel ", "command_path": [ "orders", @@ -5832,6 +5913,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid positions update-leverage", "command_path": [ "positions", @@ -5912,6 +5994,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid positions update-margin", "command_path": [ "positions", @@ -5979,6 +6062,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders schedule-cancel", "command_path": [ "orders", @@ -6074,6 +6158,7 @@ "transfers" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer spot-to-perp", "command_path": [ "transfer", @@ -6143,6 +6228,7 @@ "transfers" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer perp-to-spot", "command_path": [ "transfer", @@ -6212,6 +6298,7 @@ "transfers" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer send", "command_path": [ "transfer", @@ -6294,6 +6381,7 @@ "transfers" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer spot-send", "command_path": [ "transfer", @@ -6389,6 +6477,7 @@ "transfers" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer send-asset", "command_path": [ "transfer", @@ -6523,6 +6612,7 @@ "transfers" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer withdraw", "command_path": [ "transfer", @@ -6603,6 +6693,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking delegate", "command_path": [ "staking", @@ -6670,6 +6761,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking undelegate", "command_path": [ "staking", @@ -6737,6 +6829,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking deposit", "command_path": [ "staking", @@ -6791,6 +6884,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking withdraw", "command_path": [ "staking", @@ -6845,6 +6939,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking claim-rewards", "command_path": [ "staking", @@ -6885,6 +6980,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking link initiate", "command_path": [ "staking", @@ -6953,6 +7049,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking link finalize", "command_path": [ "staking", @@ -7023,6 +7120,7 @@ "vaults" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid vault deposit", "command_path": [ "vault", @@ -7092,6 +7190,7 @@ "vaults" ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid vault withdraw", "command_path": [ "vault", @@ -7159,6 +7258,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "local_only", "command": "hyperliquid prio bid", "command_path": [ "prio", @@ -7239,6 +7339,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid referral set ", "command_path": [ "referral", @@ -7293,6 +7394,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid referral register ", "command_path": [ "referral", @@ -7347,6 +7449,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid subscribe trades", "command_path": [ "subscribe", @@ -7427,6 +7530,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid subscribe orderbook", "command_path": [ "subscribe", @@ -7507,6 +7611,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid subscribe candles", "command_path": [ "subscribe", @@ -7600,6 +7705,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid subscribe all-mids", "command_path": [ "subscribe", @@ -7667,6 +7773,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subscribe order-updates", "command_path": [ "subscribe", @@ -7734,6 +7841,7 @@ { "aliases": [], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subscribe fills", "command_path": [ "subscribe", @@ -7801,6 +7909,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid feedback", "command_path": [ "feedback" @@ -7913,6 +8022,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid schema", "command_path": [ "schema" @@ -7966,6 +8076,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid update", "command_path": [ "update" @@ -8005,6 +8116,7 @@ { "aliases": [], "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid setup", "command_path": [ "setup" diff --git a/tests/fixtures/contracts/schema_high_risk_metadata.json b/tests/fixtures/contracts/schema_high_risk_metadata.json index f7b0c01..897ed7c 100644 --- a/tests/fixtures/contracts/schema_high_risk_metadata.json +++ b/tests/fixtures/contracts/schema_high_risk_metadata.json @@ -3,6 +3,7 @@ "commands": [ { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid builder approve", "confirmation": "prompt", "dangerous": true, @@ -14,6 +15,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid account abstraction set", "confirmation": "prompt", "dangerous": true, @@ -25,6 +27,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subaccount create", "confirmation": "none", "dangerous": true, @@ -36,6 +39,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subaccount transfer", "confirmation": "prompt", "dangerous": true, @@ -47,6 +51,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid subaccount spot-transfer", "confirmation": "prompt", "dangerous": true, @@ -58,6 +63,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account ls", "confirmation": "none", "dangerous": false, @@ -69,6 +75,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account add ", "confirmation": "prompt", "dangerous": true, @@ -80,6 +87,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account set-default ", "confirmation": "prompt", "dangerous": true, @@ -91,6 +99,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid account remove ", "confirmation": "prompt", "dangerous": true, @@ -102,6 +111,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid api-wallet create", "confirmation": "none", "dangerous": true, @@ -113,6 +123,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid api-wallet approve", "confirmation": "none", "dangerous": true, @@ -124,6 +135,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid api-wallet revoke", "confirmation": "none", "dangerous": true, @@ -135,6 +147,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet create", "confirmation": "none", "dangerous": true, @@ -146,6 +159,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet import ", "confirmation": "prompt", "dangerous": true, @@ -157,6 +171,7 @@ }, { "auth_required": true, + "bankr_signer": "address_selector_supported", "command": "hyperliquid wallet show", "confirmation": "none", "dangerous": false, @@ -168,6 +183,7 @@ }, { "auth_required": true, + "bankr_signer": "address_selector_supported", "command": "hyperliquid wallet address", "confirmation": "none", "dangerous": false, @@ -179,6 +195,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet reset", "confirmation": "prompt", "dangerous": true, @@ -190,6 +207,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet import-mnemonic ", "confirmation": "prompt", "dangerous": true, @@ -201,6 +219,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet list", "confirmation": "none", "dangerous": false, @@ -212,6 +231,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet rename ", "confirmation": "none", "dangerous": true, @@ -223,6 +243,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet delete ", "confirmation": "prompt", "dangerous": true, @@ -234,6 +255,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid wallet export ", "confirmation": "prompt", "dangerous": true, @@ -245,6 +267,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid borrowlend supply --amount ", "confirmation": "none", "dangerous": true, @@ -256,6 +279,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid borrowlend withdraw --amount ", "confirmation": "none", "dangerous": true, @@ -267,6 +291,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders create", "confirmation": "prompt", "dangerous": true, @@ -278,6 +303,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders scale", "confirmation": "prompt", "dangerous": true, @@ -289,6 +315,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders batch-create", "confirmation": "prompt", "dangerous": true, @@ -300,6 +327,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders tpsl", "confirmation": "prompt", "dangerous": true, @@ -311,6 +339,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders cancel ", "confirmation": "none", "dangerous": true, @@ -322,6 +351,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders cancel-all", "confirmation": "prompt", "dangerous": true, @@ -333,6 +363,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders modify ", "confirmation": "none", "dangerous": true, @@ -344,6 +375,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders twap-create", "confirmation": "prompt", "dangerous": true, @@ -355,6 +387,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders twap-cancel ", "confirmation": "none", "dangerous": true, @@ -366,6 +399,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid positions update-leverage", "confirmation": "none", "dangerous": true, @@ -377,6 +411,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid positions update-margin", "confirmation": "none", "dangerous": true, @@ -388,6 +423,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders schedule-cancel", "confirmation": "prompt", "dangerous": true, @@ -399,6 +435,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer spot-to-perp", "confirmation": "prompt", "dangerous": true, @@ -410,6 +447,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer perp-to-spot", "confirmation": "prompt", "dangerous": true, @@ -421,6 +459,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer send", "confirmation": "prompt", "dangerous": true, @@ -432,6 +471,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer spot-send", "confirmation": "prompt", "dangerous": true, @@ -443,6 +483,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer send-asset", "confirmation": "prompt", "dangerous": true, @@ -454,6 +495,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer withdraw", "confirmation": "prompt", "dangerous": true, @@ -465,6 +507,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking delegate", "confirmation": "none", "dangerous": true, @@ -476,6 +519,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking undelegate", "confirmation": "none", "dangerous": true, @@ -487,6 +531,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking deposit", "confirmation": "none", "dangerous": true, @@ -498,6 +543,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking withdraw", "confirmation": "none", "dangerous": true, @@ -509,6 +555,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking claim-rewards", "confirmation": "none", "dangerous": true, @@ -520,6 +567,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking link initiate", "confirmation": "prompt", "dangerous": true, @@ -531,6 +579,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid staking link finalize", "confirmation": "prompt", "dangerous": true, @@ -542,6 +591,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid vault deposit", "confirmation": "none", "dangerous": true, @@ -553,6 +603,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid vault withdraw", "confirmation": "none", "dangerous": true, @@ -564,6 +615,7 @@ }, { "auth_required": true, + "bankr_signer": "local_only", "command": "hyperliquid prio bid", "confirmation": "none", "dangerous": true, @@ -575,6 +627,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid referral set ", "confirmation": "none", "dangerous": true, @@ -586,6 +639,7 @@ }, { "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid referral register ", "confirmation": "none", "dangerous": true, @@ -597,6 +651,7 @@ }, { "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid update", "confirmation": "none", "dangerous": true, @@ -608,6 +663,7 @@ }, { "auth_required": false, + "bankr_signer": "not_applicable", "command": "hyperliquid setup", "confirmation": "prompt", "dangerous": true, diff --git a/tests/fixtures/contracts/schema_representative.json b/tests/fixtures/contracts/schema_representative.json index 2812de3..4e3b9a4 100644 --- a/tests/fixtures/contracts/schema_representative.json +++ b/tests/fixtures/contracts/schema_representative.json @@ -285,6 +285,7 @@ } ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders create", "command_path": [ "orders", @@ -430,6 +431,7 @@ "type": "object", "x-hyperliquid": { "aliases": [], + "bankr_signer": "unsupported_raw_l1_signing", "confirmation": "prompt", "confirmation_bypass": { "arg": "yes", @@ -542,6 +544,7 @@ } ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid orders modify ", "command_path": [ "orders", @@ -605,6 +608,7 @@ "type": "object", "x-hyperliquid": { "aliases": [], + "bankr_signer": "unsupported_raw_l1_signing", "confirmation": "none", "confirmation_bypass": { "arg": null, @@ -680,6 +684,7 @@ } ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid transfer send", "command_path": [ "transfer", @@ -722,6 +727,7 @@ "aliases": [ "transfers" ], + "bankr_signer": "unsupported_raw_l1_signing", "confirmation": "prompt", "confirmation_bypass": { "arg": "yes", @@ -755,6 +761,7 @@ "aliases": [], "args": [], "auth_required": true, + "bankr_signer": "address_selector_supported", "command": "hyperliquid wallet address", "command_path": [ "wallet", @@ -775,6 +782,7 @@ "type": "object", "x-hyperliquid": { "aliases": [], + "bankr_signer": "address_selector_supported", "confirmation": "none", "confirmation_bypass": { "arg": null, @@ -835,6 +843,7 @@ } ], "auth_required": true, + "bankr_signer": "unsupported_raw_l1_signing", "command": "hyperliquid borrowlend supply --amount ", "command_path": [ "borrowlend", @@ -870,6 +879,7 @@ "type": "object", "x-hyperliquid": { "aliases": [], + "bankr_signer": "unsupported_raw_l1_signing", "confirmation": "none", "confirmation_bypass": { "arg": null, @@ -930,6 +940,7 @@ } ], "auth_required": false, + "bankr_signer": "not_required", "command": "hyperliquid subscribe all-mids", "command_path": [ "subscribe", @@ -959,6 +970,7 @@ "type": "object", "x-hyperliquid": { "aliases": [], + "bankr_signer": "not_required", "confirmation": "none", "confirmation_bypass": { "arg": null, diff --git a/tests/registry_contracts.rs b/tests/registry_contracts.rs index 38f6660..ff0158b 100644 --- a/tests/registry_contracts.rs +++ b/tests/registry_contracts.rs @@ -71,6 +71,10 @@ fn registry_inventory_matches_schema_contracts() { command_value["ows_signer"], schema["json_schema"]["x-hyperliquid"]["ows_signer"], "registry/schema drift for {command_key} field ows_signer" ); + assert_eq!( + command_value["bankr_signer"], schema["json_schema"]["x-hyperliquid"]["bankr_signer"], + "registry/schema drift for {command_key} field bankr_signer" + ); assert_one_of_required_matches_schema( &command_key, &command_value["one_of_required"], diff --git a/tests/release_artifacts.rs b/tests/release_artifacts.rs index 4c0ac26..be5acc8 100644 --- a/tests/release_artifacts.rs +++ b/tests/release_artifacts.rs @@ -525,6 +525,7 @@ fn tool_catalog_materializes_registry_policy_metadata() { "raw_payload", "confirmation", "ows_signer", + "bankr_signer", ] { assert_eq!( catalog_command[field], registry_value[field], diff --git a/tests/schema_contracts.rs b/tests/schema_contracts.rs index 52ccf2b..e0a8562 100644 --- a/tests/schema_contracts.rs +++ b/tests/schema_contracts.rs @@ -42,6 +42,7 @@ fn high_risk_schema_metadata_stays_classified() { "raw_payload": schema["raw_payload"], "confirmation": schema["confirmation"], "ows_signer": schema["json_schema"]["x-hyperliquid"]["ows_signer"], + "bankr_signer": schema["json_schema"]["x-hyperliquid"]["bankr_signer"], }) }) .collect::>();