diff --git a/README.md b/README.md index 99ec030..0b9c613 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ and they can convert them back as well. ![withdraw](./res/withdraw.png) -### How to build +### How to build Requires `rust` and `cargo`: [installation instructions.](https://www.rust-lang.org/en-US/install.html) @@ -83,16 +83,20 @@ keystore = "/path/to/keystore" [home] account = "0x006e27b6a72e1f34c626762f3c4761547aff1421" -rpc_host = "http://localhost" -rpc_port = 8545 +primary_rpc_host = "http://localhost" +primary_rpc_port = 8545 +failover_rpc_host = "http://localhost" +failover_rpc_port = 8546 required_confirmations = 0 password = "home_password.txt" default_gas_price = 1_000_000_000 # 1 GWEI [foreign] account = "0x006e27b6a72e1f34c626762f3c4761547aff1421" -rpc_host = "http://localhost" -rpc_port = 9545 +primary_rpc_host = "http://localhost" +primary_rpc_port = 9545 +failover_rpc_host = "http://localhost" +failover_rpc_port = 9546 required_confirmations = 0 gas_price_oracle_url = "https://gasprice.poa.network" gas_price_speed = "instant" @@ -110,13 +114,15 @@ withdraw_confirm = { gas = 3000000 } #### Options -- `keystore` - path to a keystore directory with JSON keys +- `keystore` - path to a keystore directory with JSON keys #### home/foreign options - `home/foreign.account` - authority address on the home (**required**) -- `home/foreign.rpc_host` - RPC host (**required**) -- `home/foreign.rpc_port` - RPC port (**defaults to 8545**) +- `home/foreign.primary_rpc_host` - Primary RPC host (**required**) +- `home/foreign.primary_rpc_port` - Primary RPC port (**defaults to 8545**) +- `home/foreign.failover_rpc_host` - Failover RPC host used in the event the primary RPC host is not available. Must be specified if `failover_rpc_port` is set. +- `home/foreign.failover_rpc_port` - Failover RPC port used in the event the primary RPC host is not available. (**defaults to 8545** if `failover_rpc_host` is set.) - `home/foreign.required_confirmations` - number of confirmation required to consider transaction final on home (default: **12**) - `home/foreign.poll_interval` - specify how often home node should be polled for changes (in seconds, default: **1**) - `home/foreign.request_timeout` - specify request timeout (in seconds, default: **3600**) diff --git a/bridge/src/app.rs b/bridge/src/app.rs index 70f6980..677f0f4 100644 --- a/bridge/src/app.rs +++ b/bridge/src/app.rs @@ -1,11 +1,10 @@ use std::path::{Path, PathBuf}; use tokio_core::reactor::{Handle}; use tokio_timer::{self, Timer}; -use web3::Transport; -use error::{Error, ResultExt, ErrorKind}; -use config::Config; +use error::{Error, ErrorKind}; +use config::{Config, RpcUrl, RpcUrlKind}; use contracts::{home, foreign}; -use web3::transports::http::Http; +use web3::{Transport, transports::http::Http, error::Error as Web3Error}; use std::time::Duration; use std::sync::Arc; @@ -27,45 +26,82 @@ pub struct App where T: Transport { pub struct Connections where T: Transport { pub home: T, + pub home_url: RpcUrlKind, pub foreign: T, + pub foreign_url: RpcUrlKind, } impl Connections { - pub fn new_http(handle: &Handle, home: &str, home_concurrent_connections: usize, foreign: &str, foreign_concurrent_connections: usize) -> Result { + /// Returns new home and foreign HTTP transport connections, falling back + /// to failover urls if necessary. + pub fn new_http(handle: &Handle, home_primary: &RpcUrl, home_failover: Option<&RpcUrl>, + home_concurrent_connections: usize, foreign_primary: &RpcUrl, + foreign_failover: Option<&RpcUrl>, foreign_concurrent_connections: usize) + -> Result { + // Attempts to connect to either a primary or failover url, returning + // the transport and the url upon success. + fn connect(handle: &Handle, url_primary: &RpcUrl, url_failover: Option<&RpcUrl>, + concurrent_connections: usize) -> Result<(Http, RpcUrlKind), Web3Error> { + match Http::with_event_loop(&url_primary.to_string(), handle, concurrent_connections) { + Ok(t) => Ok((t, RpcUrlKind::Primary(url_primary.clone()))), + Err(err) => match url_failover { + Some(fo) => { + Http::with_event_loop(&fo.to_string(), handle, concurrent_connections) + .map(|h| (h, RpcUrlKind::Failover(fo.clone()))) + }, + None => Err(err), + }, + } + } - let home = Http::with_event_loop(home, handle,home_concurrent_connections) - .map_err(ErrorKind::Web3) - .map_err(Error::from) - .chain_err(||"Cannot connect to home node rpc")?; - let foreign = Http::with_event_loop(foreign, handle, foreign_concurrent_connections) - .map_err(ErrorKind::Web3) - .map_err(Error::from) - .chain_err(||"Cannot connect to foreign node rpc")?; + let (home, home_url) = connect(handle, home_primary, home_failover, home_concurrent_connections) + .map_err(|err| ErrorKind::HomeRpcConnection(err))?; + let (foreign, foreign_url) = connect(handle, foreign_primary, foreign_failover, foreign_concurrent_connections) + .map_err(|err| ErrorKind::ForeignRpcConnection(err))?; - let result = Connections { + Ok(Connections { home, - foreign - }; - Ok(result) + home_url, + foreign, + foreign_url, + }) } } -impl Connections { - pub fn as_ref(&self) -> Connections<&T> { - Connections { +/// Contains references to the fields of a `Connection`. +pub struct ConnectionsRef<'u, T> where T: Transport { + pub home: T, + pub home_url: &'u RpcUrlKind, + pub foreign: T, + pub foreign_url: &'u RpcUrlKind, +} + +impl<'u, T: Transport> ConnectionsRef<'u, T> { + pub fn as_ref(&'u self) -> ConnectionsRef<'u, &T> { + ConnectionsRef { home: &self.home, + home_url: &self.home_url, foreign: &self.foreign, + foreign_url: &self.foreign_url, } } } impl App { - pub fn new_http>(config: Config, database_path: P, handle: &Handle, running: Arc) -> Result { - let home_url:String = format!("{}:{}", config.home.rpc_host, config.home.rpc_port); - let foreign_url:String = format!("{}:{}", config.foreign.rpc_host, config.foreign.rpc_port); + pub fn new_http>(config: Config, database_path: P, handle: &Handle, + running: Arc) -> Result { + let connections = Connections::new_http( + handle, + &config.home.primary_rpc, + config.home.failover_rpc.as_ref(), + config.home.concurrent_http_requests, + &config.foreign.primary_rpc, + config.foreign.failover_rpc.as_ref(), + config.foreign.concurrent_http_requests, + )?; - let connections = Connections::new_http(handle, home_url.as_ref(), config.home.concurrent_http_requests, foreign_url.as_ref(), config.foreign.concurrent_http_requests)?; - let keystore = EthStore::open(Box::new(RootDiskDirectory::at(&config.keystore))).map_err(|e| ErrorKind::KeyStore(e))?; + let keystore = EthStore::open(Box::new(RootDiskDirectory::at(&config.keystore))) + .map_err(|e| ErrorKind::KeyStore(e))?; let keystore = AccountProvider::new(Box::new(keystore), AccountProviderSettings { enable_hardware_wallets: false, @@ -73,8 +109,10 @@ impl App { unlock_keep_secret: true, blacklisted_accounts: vec![], }); - keystore.unlock_account_permanently(config.home.account, config.home.password()?).map_err(|e| ErrorKind::AccountError(e))?; - keystore.unlock_account_permanently(config.foreign.account, config.foreign.password()?).map_err(|e| ErrorKind::AccountError(e))?; + keystore.unlock_account_permanently(config.home.account, config.home.password()?) + .map_err(|e| ErrorKind::AccountError(e))?; + keystore.unlock_account_permanently(config.foreign.account, config.foreign.password()?) + .map_err(|e| ErrorKind::AccountError(e))?; let max_timeout = config.clone().home.request_timeout.max(config.clone().foreign.request_timeout); @@ -96,3 +134,4 @@ impl App { Ok(result) } } + diff --git a/bridge/src/bridge/gas_price.rs b/bridge/src/bridge/gas_price.rs index 7988635..135a6d4 100644 --- a/bridge/src/bridge/gas_price.rs +++ b/bridge/src/bridge/gas_price.rs @@ -143,7 +143,7 @@ mod tests { use super::*; use error::{Error, ErrorKind}; use futures::{Async, future::{err, ok, FutureResult}}; - use config::{Node, NodeInfo, DEFAULT_CONCURRENCY}; + use config::{Node, NodeInfo, DEFAULT_CONCURRENCY, RpcUrl}; use tokio_timer::Timer; use std::time::Duration; use std::path::PathBuf; @@ -168,8 +168,8 @@ mod tests { request_timeout: Duration::from_secs(5), poll_interval: Duration::from_secs(1), required_confirmations: 0, - rpc_host: "https://rpc".into(), - rpc_port: 443, + primary_rpc: RpcUrl { host: "https://rpc".into(), port: 443 }, + failover_rpc: None, password: PathBuf::from("password"), info: NodeInfo::default(), gas_price_oracle_url: Some("https://gas.price".into()), @@ -211,8 +211,10 @@ mod tests { request_timeout: Duration::from_secs(5), poll_interval: Duration::from_secs(1), required_confirmations: 0, - rpc_host: "https://rpc".into(), - rpc_port: 443, + // rpc_host: "https://rpc".into(), + // rpc_port: 443, + primary_rpc: RpcUrl { host: "https://rpc".into(), port: 443 }, + failover_rpc: None, password: PathBuf::from("password"), info: NodeInfo::default(), gas_price_oracle_url: Some("https://gas.price".into()), @@ -254,8 +256,8 @@ mod tests { request_timeout: Duration::from_secs(5), poll_interval: Duration::from_secs(1), required_confirmations: 0, - rpc_host: "https://rpc".into(), - rpc_port: 443, + primary_rpc: RpcUrl { host: "https://rpc".into(), port: 443 }, + failover_rpc: None, password: PathBuf::from("password"), info: NodeInfo::default(), gas_price_oracle_url: Some("https://gas.price".into()), @@ -296,8 +298,8 @@ mod tests { request_timeout: Duration::from_secs(5), poll_interval: Duration::from_secs(1), required_confirmations: 0, - rpc_host: "https://rpc".into(), - rpc_port: 443, + primary_rpc: RpcUrl { host: "https://rpc".into(), port: 443 }, + failover_rpc: None, password: PathBuf::from("password"), info: NodeInfo::default(), gas_price_oracle_url: Some("https://gas.price".into()), @@ -338,8 +340,8 @@ mod tests { request_timeout: Duration::from_secs(5), poll_interval: Duration::from_secs(1), required_confirmations: 0, - rpc_host: "https://rpc".into(), - rpc_port: 443, + primary_rpc: RpcUrl { host: "https://rpc".into(), port: 443 }, + failover_rpc: None, password: PathBuf::from("password"), info: NodeInfo::default(), gas_price_oracle_url: Some("https://gas.price".into()), diff --git a/bridge/src/bridge/nonce.rs b/bridge/src/bridge/nonce.rs index e9007db..0a05f5c 100644 --- a/bridge/src/bridge/nonce.rs +++ b/bridge/src/bridge/nonce.rs @@ -5,7 +5,7 @@ use web3::types::{U256, H256, Bytes}; use ethcore_transaction::Transaction; use api::{self, ApiCall}; use error::{Error, ErrorKind}; -use config::Node; +use config::{Node, RpcUrlKind}; use transaction::prepare_raw_transaction; use app::App; use std::sync::Arc; @@ -32,6 +32,8 @@ enum NonceCheckState { pub struct NonceCheck { app: Arc>, transport: T, + /// Used for logging: + rpc_url: RpcUrlKind, state: NonceCheckState, node: Node, transaction: Transaction, @@ -48,11 +50,14 @@ impl Debug for NonceCheck { } -pub fn send_transaction_with_nonce(transport: T, app: Arc>, node: Node, transaction: Transaction, chain_id: u64, sender: S) -> NonceCheck { +pub fn send_transaction_with_nonce(transport: T, rpc_url: RpcUrlKind, app: Arc>, + node: Node, transaction: Transaction, chain_id: u64, sender: S) -> NonceCheck + where T: Transport + Clone, S: TransactionSender { NonceCheck { app, state: NonceCheckState::Ready, transport, + rpc_url, node, transaction, chain_id, @@ -108,7 +113,8 @@ impl Future for NonceCheck { NonceCheckState::Reacquire } else if rpc_err.code == rpc::ErrorCode::ServerError(-32010) && rpc_err.message.ends_with("already imported.") { let hash = self.transaction.hash(Some(self.chain_id)); - info!("{} already imported on {}, skipping", hash, self.node.rpc_host); + // info!("{} already imported on {}, skipping", hash, self.node.rpc_host); + info!("{} already imported on {}, skipping", hash, self.rpc_url); return Ok(Async::Ready(self.sender.ignore(hash))) } else { return Err(ErrorKind::Web3(web3::error::ErrorKind::Rpc(rpc_err).into()).into()); diff --git a/bridge/src/config.rs b/bridge/src/config.rs index b092b28..df2e24a 100644 --- a/bridge/src/config.rs +++ b/bridge/src/config.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::path::{Path, PathBuf}; use std::fs; use std::io::Read; @@ -72,8 +73,12 @@ pub struct Node { pub request_timeout: Duration, pub poll_interval: Duration, pub required_confirmations: usize, - pub rpc_host: String, - pub rpc_port: u16, + // pub primary_rpc_host: String, + // pub primary_rpc_port: u16, + // pub failover_rpc_host: Option, + // pub failover_rpc_port: Option, + pub primary_rpc: RpcUrl, + pub failover_rpc: Option, pub password: PathBuf, pub info: NodeInfo, pub gas_price_oracle_url: Option, @@ -122,16 +127,44 @@ impl Node { let default_gas_price = node.default_gas_price.unwrap_or(DEFAULT_GAS_PRICE_WEI); let concurrent_http_requests = node.concurrent_http_requests.unwrap_or(DEFAULT_CONCURRENCY); - let rpc_host = node.rpc_host.unwrap(); - - if !rpc_host.starts_with("https://") { - if !allow_insecure_rpc_endpoints { - return Err(ErrorKind::ConfigError(format!("RPC endpoints must use TLS, {} doesn't", rpc_host)).into()); - } else { - warn!("RPC endpoints must use TLS, {} doesn't", rpc_host); + // Ensures host url scheme is correct. + fn check_host(host: String, allow_insecure_rpc_endpoints: bool) -> Result { + if !host.starts_with("https://") { + if !allow_insecure_rpc_endpoints { + return Err(ErrorKind::ConfigError(format!("RPC endpoints must use TLS, {} doesn't", host)).into()); + } else { + warn!("RPC endpoints must use TLS, {} doesn't", host); + } } + Ok(host) } + // Check primary RPC host: + let primary_rpc_host = check_host( + node.primary_rpc_host.expect("Primary RPC host not specified."), + allow_insecure_rpc_endpoints)?; + + let primary_rpc = RpcUrl { + host: primary_rpc_host, + port: node.primary_rpc_port.unwrap_or(DEFAULT_RPC_PORT), + }; + + // If failover RPC host is defined, determine port: + let failover_rpc = match node.failover_rpc_host { + Some(host) => { + Some(RpcUrl { + host: check_host(host, allow_insecure_rpc_endpoints)?, + port: node.failover_rpc_port.unwrap_or(DEFAULT_RPC_PORT), + }) + }, + None => { + // Ensure port is not specified without a host: + assert!(node.failover_rpc_port.is_none(), + "Failover RPC port specified without a failover host."); + None + }, + }; + let result = Node { account: node.account, #[cfg(feature = "deploy")] @@ -146,8 +179,8 @@ impl Node { request_timeout: Duration::from_secs(node.request_timeout.unwrap_or(DEFAULT_TIMEOUT)), poll_interval: Duration::from_secs(node.poll_interval.unwrap_or(DEFAULT_POLL_INTERVAL)), required_confirmations: node.required_confirmations.unwrap_or(DEFAULT_CONFIRMATIONS), - rpc_host, - rpc_port: node.rpc_port.unwrap_or(DEFAULT_RPC_PORT), + primary_rpc, + failover_rpc, password: node.password, info: Default::default(), gas_price_oracle_url, @@ -257,6 +290,45 @@ impl GasPriceSpeed { } } +/// A RPC url with its kind, primary or failover. +#[derive(Clone, Debug, PartialEq)] +pub enum RpcUrlKind { + Primary(RpcUrl), + Failover(RpcUrl), +} + +impl RpcUrlKind { + /// Returns the contained url, regarless of variant. + pub fn url(&self) -> &RpcUrl { + match self { + RpcUrlKind::Primary(ref u) => u, + RpcUrlKind::Failover(ref u) => u, + } + } +} + +impl fmt::Display for RpcUrlKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RpcUrlKind::Primary(u) => write!(f, "Primary({})", u), + RpcUrlKind::Failover(u) => write!(f, "Failover({})", u), + } + } +} + +/// A host name and port for an RPC service. +#[derive(Clone, Debug, PartialEq)] +pub struct RpcUrl { + pub host: String, + pub port: u16, +} + +impl fmt::Display for RpcUrl { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.host, self.port) + } +} + /// Some config values may not be defined in `toml` file, but they should be specified at runtime. /// `load` module separates `Config` representation in file with optional from the one used /// in application. @@ -285,8 +357,10 @@ mod load { pub request_timeout: Option, pub poll_interval: Option, pub required_confirmations: Option, - pub rpc_host: Option, - pub rpc_port: Option, + pub primary_rpc_host: Option, + pub primary_rpc_port: Option, + pub failover_rpc_host: Option, + pub failover_rpc_port: Option, pub password: PathBuf, pub gas_price_oracle_url: Option, pub gas_price_speed: Option, @@ -334,7 +408,7 @@ mod tests { use std::time::Duration; #[cfg(feature = "deploy")] use rustc_hex::FromHex; - use super::{Config, Node, Transactions, Authorities}; + use super::{Config, Node, Transactions, Authorities, RpcUrl}; #[cfg(feature = "deploy")] use super::ContractConfig; #[cfg(feature = "deploy")] @@ -350,14 +424,89 @@ keystore = "/keys" account = "0x1B68Cb0B50181FC4006Ce572cF346e596E51818b" poll_interval = 2 required_confirmations = 100 -rpc_host = "127.0.0.1" -rpc_port = 8545 +primary_rpc_host = "127.0.0.1" +primary_rpc_port = 8545 +password = "password" + +[foreign] +account = "0x0000000000000000000000000000000000000001" +primary_rpc_host = "127.0.0.1" +primary_rpc_port = 8545 +password = "password" + +[authorities] +required_signatures = 2 + +[transactions] +"#; + + #[allow(unused_mut)] + let mut expected = Config { + txs: Transactions::default(), + home: Node { + account: "1B68Cb0B50181FC4006Ce572cF346e596E51818b".into(), + poll_interval: Duration::from_secs(2), + request_timeout: Duration::from_secs(DEFAULT_TIMEOUT), + required_confirmations: 100, + primary_rpc: RpcUrl { host: "127.0.0.1".into(), port: 8545 }, + failover_rpc: None, + password: "password".into(), + info: Default::default(), + gas_price_oracle_url: None, + gas_price_speed: DEFAULT_GAS_PRICE_SPEED, + gas_price_timeout: Duration::from_secs(DEFAULT_GAS_PRICE_TIMEOUT_SECS), + default_gas_price: DEFAULT_GAS_PRICE_WEI, + concurrent_http_requests: DEFAULT_CONCURRENCY, + }, + foreign: Node { + account: "0000000000000000000000000000000000000001".into(), + poll_interval: Duration::from_secs(1), + request_timeout: Duration::from_secs(DEFAULT_TIMEOUT), + required_confirmations: 12, + primary_rpc: RpcUrl { host: "127.0.0.1".into(), port: 8545 }, + failover_rpc: None, + password: "password".into(), + info: Default::default(), + gas_price_oracle_url: None, + gas_price_speed: DEFAULT_GAS_PRICE_SPEED, + gas_price_timeout: Duration::from_secs(DEFAULT_GAS_PRICE_TIMEOUT_SECS), + default_gas_price: DEFAULT_GAS_PRICE_WEI, + concurrent_http_requests: DEFAULT_CONCURRENCY, + }, + authorities: Authorities { + #[cfg(feature = "deploy")] + accounts: vec![ + ], + required_signatures: 2, + }, + keystore: "/keys/".into(), + }; + + let config = Config::load_from_str(toml, true).unwrap(); + assert_eq!(expected, config); + } + + #[test] + fn load_full_setup_from_str_failover() { + let toml = r#" +keystore = "/keys" + +[home] +account = "0x1B68Cb0B50181FC4006Ce572cF346e596E51818b" +poll_interval = 2 +required_confirmations = 100 +primary_rpc_host = "127.0.0.1" +primary_rpc_port = 8545 +failover_rpc_host = "127.0.0.2" +failover_rpc_port = 8546 password = "password" [foreign] account = "0x0000000000000000000000000000000000000001" -rpc_host = "127.0.0.1" -rpc_port = 8545 +primary_rpc_host = "127.0.0.3" +primary_rpc_port = 8547 +failover_rpc_host = "127.0.0.4" +failover_rpc_port = 8548 password = "password" [authorities] @@ -374,8 +523,8 @@ required_signatures = 2 poll_interval: Duration::from_secs(2), request_timeout: Duration::from_secs(DEFAULT_TIMEOUT), required_confirmations: 100, - rpc_host: "127.0.0.1".into(), - rpc_port: 8545, + primary_rpc: RpcUrl { host: "127.0.0.1".into(), port: 8545 }, + failover_rpc: Some(RpcUrl { host: "127.0.0.2".into(), port: 8546 }), password: "password".into(), info: Default::default(), gas_price_oracle_url: None, @@ -389,8 +538,8 @@ required_signatures = 2 poll_interval: Duration::from_secs(1), request_timeout: Duration::from_secs(DEFAULT_TIMEOUT), required_confirmations: 12, - rpc_host: "127.0.0.1".into(), - rpc_port: 8545, + primary_rpc: RpcUrl { host: "127.0.0.3".into(), port: 8547 }, + failover_rpc: Some(RpcUrl { host: "127.0.0.4".into(), port: 8548 }), password: "password".into(), info: Default::default(), gas_price_oracle_url: None, @@ -419,12 +568,12 @@ keystore = "/keys/" [home] account = "0x1B68Cb0B50181FC4006Ce572cF346e596E51818b" -rpc_host = "" +primary_rpc_host = "" password = "password" [foreign] account = "0x0000000000000000000000000000000000000001" -rpc_host = "" +primary_rpc_host = "" password = "password" [authorities] @@ -437,8 +586,8 @@ required_signatures = 2 poll_interval: Duration::from_secs(1), request_timeout: Duration::from_secs(DEFAULT_TIMEOUT), required_confirmations: 12, - rpc_host: "".into(), - rpc_port: 8545, + primary_rpc: RpcUrl { host: "".into(), port: 8545 }, + failover_rpc: None, password: "password".into(), info: Default::default(), gas_price_oracle_url: None, @@ -452,8 +601,8 @@ required_signatures = 2 poll_interval: Duration::from_secs(1), request_timeout: Duration::from_secs(DEFAULT_TIMEOUT), required_confirmations: 12, - rpc_host: "".into(), - rpc_port: 8545, + primary_rpc: RpcUrl { host: "".into(), port: 8545 }, + failover_rpc: None, password: "password".into(), info: Default::default(), gas_price_oracle_url: None, diff --git a/bridge/src/error.rs b/bridge/src/error.rs index ce69d0c..10ef984 100644 --- a/bridge/src/error.rs +++ b/bridge/src/error.rs @@ -66,6 +66,14 @@ error_chain! { description("config error") display("{}", err) } + HomeRpcConnection(err: web3::Error) { + description("Cannot connect to home RPC node") + display("Cannot connect to home RPC node: {:?}", err), + } + ForeignRpcConnection(err: web3::Error) { + description("Cannot connect to foreign RPC node") + display("Cannot connect to foreign RPC node: {:?}", err), + } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 3fbf22d..479ff25 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -136,8 +136,14 @@ fn execute(command: I, running: Arc) -> Result(command: I, running: Arc) -> Result