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