1use std::cmp;
30use std::convert::TryFrom;
31use std::ops::Deref;
32use std::sync::Arc;
33use std::time::Duration;
34
35use bitcoin::{FeeRate, Network};
36use log::warn;
37use tokio::sync::RwLock;
38use tonic::metadata::AsciiMetadataValue;
39use tonic::metadata::errors::InvalidMetadataValue;
40use tonic::service::interceptor::{InterceptedService, Interceptor};
41
42use ark::ArkInfo;
43
44use crate::{
45 mailbox, protos, ArkServiceClient, ConvertError, RequestExt,
46 MAX_PROTOCOL_VERSION, MIN_PROTOCOL_VERSION,
47};
48
49
50#[cfg(all(feature = "tonic-native", feature = "tonic-web"))]
51compile_error!("features `tonic-native` and `tonic-web` are mutually exclusive");
52
53#[cfg(all(feature = "socks5-proxy", not(feature = "tonic-native")))]
54compile_error!("the `socks5-proxy` feature is only usable in conjunction with `tonic-native`");
55
56
57#[deprecated(
59 since = "0.2.4",
60 note = "access tokens are not enforced by the server; this header will be removed",
61)]
62pub const ACCESS_TOKEN_HEADER: &str = "ark-access-token";
63pub const USER_AGENT_HEADER: &str = "x-user-agent";
73pub const NO_TRANSPORT_BACKEND_MESSAGE: &str =
75 "no Ark RPC transport backend compiled in this build; enable `bark-server-rpc/tonic-native` or `bark-server-rpc/tonic-web`";
76
77pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10 * 60);
79
80
81#[cfg(feature = "tonic-native")]
82mod transport {
83 use std::str::FromStr;
84 use std::time::Duration;
85
86 use http::Uri;
87 use log::info;
88 use tonic::transport::{Channel, Endpoint};
89
90 use super::CreateEndpointError;
91
92 pub type Transport = Channel;
93
94 pub async fn connect(address: &str) -> Result<Transport, CreateEndpointError> {
100 Ok(create_endpoint(address)?.connect().await?)
101 }
102
103 #[cfg(feature = "socks5-proxy")]
105 pub async fn connect_with_proxy(
106 address: &str,
107 proxy: &str,
108 ) -> Result<Transport, CreateEndpointError> {
109 use hyper_socks2::SocksConnector;
110 use hyper_util::client::legacy::connect::HttpConnector;
111
112 let endpoint = create_endpoint(address)?;
113 let proxy_uri = proxy.parse::<Uri>().map_err(CreateEndpointError::InvalidProxyUri)?;
114 let connector = {
115 let mut http = HttpConnector::new();
118 http.enforce_http(false);
119 SocksConnector {
120 proxy_addr: proxy_uri,
121 auth: None,
122 connector: http,
123 }
124 };
125 info!("Connecting to Ark server via SOCKS5 proxy {}...", proxy);
126 Ok(endpoint.connect_with_connector(connector).await?)
127 }
128
129 fn create_endpoint(address: &str) -> Result<Endpoint, CreateEndpointError> {
133 let uri = Uri::from_str(address)?;
134
135 let scheme = uri.scheme_str().unwrap_or("");
136 if scheme != "http" && scheme != "https" {
137 return Err(CreateEndpointError::InvalidScheme(scheme.to_string()));
138 }
139
140 #[cfg_attr(not(any(feature = "tls-native-roots", feature = "tls-webpki-roots")), allow(unused_mut))]
141 let mut endpoint = Channel::builder(uri.clone())
142 .http2_keep_alive_interval(Duration::from_secs(20))
144 .keep_alive_timeout(Duration::from_secs(60)) .keep_alive_while_idle(true);
147
148 #[cfg(any(feature = "tls-native-roots", feature = "tls-webpki-roots"))]
149 if scheme == "https" {
150 use tonic::transport::ClientTlsConfig;
151
152 info!("Connecting to Ark server at {} using TLS...", address);
153 let uri_auth = uri.clone().into_parts().authority
154 .ok_or(CreateEndpointError::MissingAuthority)?;
155 let domain = uri_auth.host();
156
157 let tls_config = ClientTlsConfig::new()
158 .with_enabled_roots()
159 .domain_name(domain);
160 endpoint = endpoint.tls_config(tls_config).map_err(CreateEndpointError::Transport)?;
161 return Ok(endpoint);
162 }
163 #[cfg(not(any(feature = "tls-native-roots", feature = "tls-webpki-roots")))]
164 if scheme == "https" {
165 return Err(CreateEndpointError::InvalidScheme(
166 "Missing TLS roots, https is unsupported".to_owned(),
167 ));
168 }
169 info!("Connecting to Ark server at {} without TLS...", address);
170 Ok(endpoint)
171 }
172}
173
174#[cfg(feature = "tonic-web")]
175mod transport {
176 use super::CreateEndpointError;
177 use tonic_web_wasm_client::Client as WasmClient;
178
179 pub type Transport = WasmClient;
180
181 pub async fn connect(address: &str) -> Result<Transport, CreateEndpointError> {
182 Ok(tonic_web_wasm_client::Client::new(address.to_string()))
183 }
184}
185
186#[cfg(not(any(feature = "tonic-native", feature = "tonic-web")))]
190mod transport {
191 use std::convert::Infallible;
192 use std::future::{ready, Ready};
193 use std::task::{Context, Poll};
194
195 use http::{Request, Response};
196 use tonic::Status;
197 use tonic::body::Body;
198 use tonic::codegen::Service;
199
200 use super::NO_TRANSPORT_BACKEND_MESSAGE;
201
202 pub async fn connect(_address: &str) -> Result<Transport, crate::client::CreateEndpointError> {
203 Err(crate::client::CreateEndpointError::NoTransportBackend)
204 }
205
206 #[derive(Debug, Clone, Default)]
207 pub struct Transport;
208
209 impl Service<Request<Body>> for Transport {
210 type Response = Response<Body>;
211 type Error = Infallible;
212 type Future = Ready<Result<Self::Response, Self::Error>>;
213
214 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
215 Poll::Ready(Ok(()))
216 }
217
218 fn call(&mut self, _req: Request<Body>) -> Self::Future {
219 let status = Status::failed_precondition(NO_TRANSPORT_BACKEND_MESSAGE);
220 ready(Ok(status.into_http::<Body>()))
221 }
222 }
223}
224
225
226#[derive(Debug, thiserror::Error)]
227#[error("failed to create gRPC endpoint: {msg}")]
228pub enum CreateEndpointError {
229 #[error("{NO_TRANSPORT_BACKEND_MESSAGE}")]
230 NoTransportBackend,
231 #[error("failed to parse Ark server as a URI")]
232 InvalidUri(#[from] http::uri::InvalidUri),
233 #[error("Ark server scheme must be either http or https. Found: {0}")]
234 InvalidScheme(String),
235 #[error("Ark server URI is missing an authority part")]
236 MissingAuthority,
237 #[cfg(feature = "tonic-native")]
238 #[error(transparent)]
239 Transport(#[from] tonic::transport::Error),
240 #[cfg(feature = "socks5-proxy")]
241 #[error("invalid SOCKS5 proxy URI: {0:#}")]
242 InvalidProxyUri(http::uri::InvalidUri),
243}
244
245#[derive(Debug, thiserror::Error)]
246#[error("failed to connect to Ark server: {msg}")]
247pub enum ConnectError {
248 #[error("missing info '{0}' to connect")]
249 MissingInfo(&'static str),
250 #[deprecated(
251 since = "0.2.4",
252 note = "access tokens are not enforced by the server; this variant will be removed",
253 )]
254 #[error("invalid access token: {0}")]
255 InvalidAccessToken(#[source] InvalidMetadataValue),
256 #[error("invalid user agent: {0}")]
257 InvalidUserAgent(#[source] InvalidMetadataValue),
258 #[error(transparent)]
259 CreateEndpoint(#[from] CreateEndpointError),
260 #[error("handshake request failed: {0}")]
261 Handshake(tonic::Status),
262 #[error("version mismatch. Client max is: {client_max}, server min is: {server_min}")]
263 ProtocolVersionMismatchClientTooOld { client_max: u64, server_min: u64 },
264 #[error("version mismatch. Client min is: {client_min}, server max is: {server_max}")]
265 ProtocolVersionMismatchServerTooOld { client_min: u64, server_max: u64 },
266 #[error("error getting ark info: {0}")]
267 GetArkInfo(tonic::Status),
268 #[error("invalid ark info from ark server: {0}")]
269 InvalidArkInfo(#[from] ConvertError),
270 #[error("network mismatch. Expected: {expected}, Got: {got}")]
271 NetworkMismatch { expected: Network, got: Network },
272 #[error("error getting offboard fee rate: {0}")]
273 GetOffboardFeeRate(tonic::Status),
274 #[error("tokio channel error: {0}")]
275 Tokio(#[from] tokio::sync::oneshot::error::RecvError),
276}
277
278#[derive(Clone)]
284#[deprecated(since = "0.1.3", note = "should not be used directly")]
285pub struct ProtocolVersionInterceptor {
286 pver: u64,
287}
288
289#[allow(deprecated)]
290impl tonic::service::Interceptor for ProtocolVersionInterceptor {
291 fn call(&mut self, mut req: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
292 #[allow(deprecated)]
293 req.set_pver(self.pver);
294 Ok(req)
295 }
296}
297
298#[derive(Clone)]
306pub struct ArkServiceInterceptor {
307 pver: Option<u64>,
308 access_token: Option<AsciiMetadataValue>,
309 user_agent: AsciiMetadataValue,
310}
311
312impl tonic::service::Interceptor for ArkServiceInterceptor {
313 fn call(&mut self, mut req: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
314 req.set_default_timeout(DEFAULT_REQUEST_TIMEOUT);
315 if let Some(pver) = self.pver {
316 req.set_pver(pver);
317 }
318 if let Some(ref access_token) = self.access_token {
319 #[allow(deprecated)]
320 req.metadata_mut().insert(ACCESS_TOKEN_HEADER, access_token.clone());
321 }
322 req.metadata_mut().insert(USER_AGENT_HEADER, self.user_agent.clone());
323 Ok(req)
324 }
325}
326
327pub struct ArkInfoHandle {
331 pub info: ArkInfo,
332 pub waiter: Option<tokio::sync::oneshot::Receiver<Result<ArkInfo, ConnectError>>>,
333}
334
335impl Deref for ArkInfoHandle {
336 type Target = ArkInfo;
337
338 fn deref(&self) -> &Self::Target {
339 &self.info
340 }
341}
342
343pub struct ServerInfo {
344 pub pver: u64,
348 pub info: ArkInfo,
350}
351
352impl ServerInfo {
353 pub fn new(pver: u64, info: ArkInfo) -> Self {
354 Self { pver, info }
355 }
356}
357
358#[derive(Default)]
359pub struct ServerConnectionBuilder {
360 address: Option<String>,
361 network: Option<Network>,
362 #[cfg(feature = "socks5-proxy")]
363 proxy: Option<String>,
364 access_token: Option<String>,
365 user_agent: Option<String>,
366}
367
368impl ServerConnectionBuilder {
369 pub fn address(mut self, address: impl Into<String>) -> Self {
370 self.address = Some(address.into());
371 self
372 }
373
374 pub fn network(mut self, network: Network) -> Self {
375 self.network = Some(network);
376 self
377 }
378
379 #[cfg(feature = "socks5-proxy")]
380 pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
381 self.proxy = Some(proxy.into());
382 self
383 }
384
385 #[deprecated(
386 since = "0.2.4",
387 note = "access tokens are not enforced by the server; this method will be removed",
388 )]
389 pub fn access_token(mut self, access_token: impl Into<String>) -> Self {
390 self.access_token = Some(access_token.into());
391 self
392 }
393
394 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
400 self.user_agent = Some(user_agent.into());
401 self
402 }
403
404 pub async fn connect(self) -> Result<ServerConnection, ConnectError> {
405 ServerConnection::inner_connect(self).await
406 }
407}
408
409#[derive(Clone)]
416pub struct ServerConnection {
417 info: Arc<RwLock<ServerInfo>>,
418 pub client: ArkServiceClient<InterceptedService<transport::Transport, ArkServiceInterceptor>>,
420 pub mailbox_client: mailbox::MailboxServiceClient<InterceptedService<transport::Transport, ArkServiceInterceptor>>,
422}
423
424impl ServerConnection {
425 fn handshake_req() -> protos::HandshakeRequest {
426 protos::HandshakeRequest {
427 bark_version: Some(env!("CARGO_PKG_VERSION").into()),
428 }
429 }
430
431 pub fn builder() -> ServerConnectionBuilder {
449 ServerConnectionBuilder::default()
450 }
451
452 async fn inner_connect(builder: ServerConnectionBuilder) -> Result<ServerConnection, ConnectError> {
454 let address = builder.address.ok_or(ConnectError::MissingInfo("address"))?;
455 let network = builder.network.ok_or(ConnectError::MissingInfo("network"))?;
456
457 #[cfg(feature = "socks5-proxy")]
458 let transport = if let Some(proxy) = builder.proxy {
459 transport::connect_with_proxy(&address, &proxy).await?
460 } else {
461 transport::connect(&address).await?
462 };
463 #[cfg(not(feature = "socks5-proxy"))]
464 let transport = transport::connect(&address).await?;
465
466 let user_agent = builder.user_agent
467 .unwrap_or_else(|| format!("bark/{}", env!("CARGO_PKG_VERSION")));
468 let user_agent: AsciiMetadataValue = user_agent.try_into()
469 .map_err(ConnectError::InvalidUserAgent)?;
470
471 let mut interceptor = ArkServiceInterceptor {
472 pver: None,
473 #[allow(deprecated)]
474 access_token: builder.access_token
475 .map(AsciiMetadataValue::try_from)
476 .transpose()
477 .map_err(ConnectError::InvalidAccessToken)?,
478 user_agent,
479 };
480
481 let mut handshake_client = ArkServiceClient::with_interceptor(transport.clone(), interceptor.clone());
482 let handshake = handshake_client.handshake(Self::handshake_req()).await
483 .map_err(ConnectError::Handshake)?.into_inner();
484
485 let pver = check_handshake(handshake)?;
486 interceptor.pver = Some(pver);
487
488 let mut client = ArkServiceClient::with_interceptor(transport.clone(), interceptor.clone())
489 .max_decoding_message_size(64 * 1024 * 1024); let info = client.ark_info(network).await?;
492
493 let mailbox_client = mailbox::MailboxServiceClient::with_interceptor(transport, interceptor)
494 .max_decoding_message_size(64 * 1024 * 1024); let info = Arc::new(RwLock::new(ServerInfo::new(pver, info)));
497 Ok(ServerConnection {
498 info,
499 client,
500 mailbox_client,
501 })
502 }
503
504 #[deprecated(since = "0.1.3", note = "use builder() instead")]
505 pub async fn connect(
506 address: &str,
507 network: Network,
508 ) -> Result<ServerConnection, ConnectError> {
509 Self::builder().address(address).network(network).connect().await
510 }
511
512 #[cfg(feature = "socks5-proxy")]
513 #[deprecated(since = "0.1.3", note = "use builder() instead")]
514 pub async fn connect_via_proxy(
515 address: &str,
516 network: Network,
517 proxy: &str,
518 ) -> Result<ServerConnection, ConnectError> {
519 Self::builder().address(address).network(network).proxy(proxy).connect().await
520 }
521
522 pub async fn check_connection(&self) -> Result<(), ConnectError> {
524 let mut client = self.client.clone();
525 let handshake = client.handshake(Self::handshake_req()).await
526 .map_err(ConnectError::Handshake)?.into_inner();
527 check_handshake(handshake)?;
528 Ok(())
529 }
530
531 pub async fn ark_info(&self) -> ArkInfo {
533 self.info.read().await.info.clone()
534 }
535
536 pub async fn offboard_feerate(&self) -> Result<FeeRate, ConnectError> {
538 let resp = self.client.clone()
539 .get_offboard_fee_rate(protos::Empty {}).await
540 .map_err(ConnectError::GetOffboardFeeRate)?
541 .into_inner();
542 Ok(FeeRate::from_sat_per_kwu(resp.sat_vkb / 4))
543 }
544}
545trait ArkServiceClientExt {
546 async fn ark_info(&mut self, network: Network) -> Result<ArkInfo, ConnectError>;
547}
548
549impl<I: Interceptor> ArkServiceClientExt for ArkServiceClient<InterceptedService<transport::Transport, I>> {
550 async fn ark_info(&mut self, network: Network) -> Result<ArkInfo, ConnectError> {
551 let res = self.get_ark_info(protos::Empty {}).await
552 .map_err(ConnectError::GetArkInfo)?;
553 let info = ArkInfo::try_from(res.into_inner())
554 .map_err(ConnectError::InvalidArkInfo)?;
555 if network != info.network {
556 return Err(ConnectError::NetworkMismatch { expected: network, got: info.network });
557 }
558
559 Ok(info)
560 }
561}
562
563fn check_handshake(handshake: protos::HandshakeResponse) -> Result<u64, ConnectError> {
564 if let Some(ref msg) = handshake.psa {
565 warn!("Message from Ark server: \"{}\"", msg);
566 }
567
568 if MAX_PROTOCOL_VERSION < handshake.min_protocol_version {
569 return Err(ConnectError::ProtocolVersionMismatchClientTooOld {
570 client_max: MAX_PROTOCOL_VERSION, server_min: handshake.min_protocol_version
571 });
572 }
573 if MIN_PROTOCOL_VERSION > handshake.max_protocol_version {
574 return Err(ConnectError::ProtocolVersionMismatchServerTooOld {
575 client_min: MIN_PROTOCOL_VERSION, server_max: handshake.max_protocol_version
576 });
577 }
578
579 let pver = cmp::min(MAX_PROTOCOL_VERSION, handshake.max_protocol_version);
580 assert!((MIN_PROTOCOL_VERSION..=MAX_PROTOCOL_VERSION).contains(&pver));
581 assert!((handshake.min_protocol_version..=handshake.max_protocol_version).contains(&pver));
582
583 Ok(pver)
584}
585
586#[cfg(test)]
587mod tests {
588 use super::{CreateEndpointError, NO_TRANSPORT_BACKEND_MESSAGE};
589
590 #[test]
591 fn no_transport_backend_error_mentions_feature_selection() {
592 let err = CreateEndpointError::NoTransportBackend;
593 assert_eq!(err.to_string(), NO_TRANSPORT_BACKEND_MESSAGE);
594 assert!(err.to_string().contains("bark-server-rpc/tonic-native"));
595 assert!(err.to_string().contains("bark-server-rpc/tonic-web"));
596 }
597}