fedimint_core/module/
mod.rs

1//! Core module system traits and types.
2//!
3//! Fedimint supports modules to allow extending its functionality.
4//! Some of the standard functionality is implemented in form of modules as
5//! well. This rust module houses the core trait
6//! [`fedimint_core::module::ModuleCommon`] used by both the server and client
7//! side module traits. Specific server and client traits exist in their
8//! respective crates.
9//!
10//! The top level server-side types are:
11//!
12//! * `fedimint_server::core::ServerModuleInit`
13//! * `fedimint_server::core::ServerModule`
14//!
15//! Top level client-side types are:
16//!
17//! * `ClientModuleInit` (in `fedimint_client`)
18//! * `ClientModule` (in `fedimint_client`)
19pub mod audit;
20pub mod registry;
21
22use std::fmt::{self, Debug, Formatter};
23use std::marker::PhantomData;
24use std::pin::Pin;
25use std::sync::Arc;
26use std::sync::atomic::{AtomicU64, Ordering};
27
28use fedimint_logging::LOG_NET_API;
29use futures::Future;
30use jsonrpsee_core::JsonValue;
31use registry::ModuleRegistry;
32use serde::{Deserialize, Serialize};
33use tracing::Instrument;
34
35// TODO: Make this module public and remove theDkgPeerMessage`pub use` below
36mod version;
37pub use self::version::*;
38use crate::config::P2PMessage;
39use crate::core::{
40    ClientConfig, Decoder, DecoderBuilder, Input, InputError, ModuleConsensusItem,
41    ModuleInstanceId, ModuleKind, Output, OutputError, OutputOutcome,
42};
43use crate::db::{
44    Committable, Database, DatabaseKey, DatabaseKeyWithNotify, DatabaseRecord, DatabaseTransaction,
45    NonCommittable,
46};
47use crate::encoding::{Decodable, DecodeError, Encodable};
48use crate::fmt_utils::AbbreviateHexBytes;
49use crate::net::peers::DynP2PConnections;
50use crate::task::MaybeSend;
51use crate::{Amount, PeerId, apply, async_trait_maybe_send, maybe_add_send, maybe_add_send_sync};
52
53#[derive(Debug, PartialEq, Eq)]
54pub struct InputMeta {
55    pub amount: TransactionItemAmount,
56    pub pub_key: secp256k1::PublicKey,
57}
58
59/// Information about the amount represented by an input or output.
60///
61/// * For **inputs** the amount is funding the transaction while the fee is
62///   consuming funding
63/// * For **outputs** the amount and the fee consume funding
64#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
65pub struct TransactionItemAmount {
66    pub amount: Amount,
67    pub fee: Amount,
68}
69
70impl TransactionItemAmount {
71    pub const ZERO: Self = Self {
72        amount: Amount::ZERO,
73        fee: Amount::ZERO,
74    };
75}
76
77/// All requests from client to server contain these fields
78#[derive(Debug, Serialize, Deserialize, Clone)]
79pub struct ApiRequest<T> {
80    /// Hashed user password if the API requires authentication
81    pub auth: Option<ApiAuth>,
82    /// Parameters required by the API
83    pub params: T,
84}
85
86pub type ApiRequestErased = ApiRequest<JsonValue>;
87
88impl Default for ApiRequestErased {
89    fn default() -> Self {
90        Self {
91            auth: None,
92            params: JsonValue::Null,
93        }
94    }
95}
96
97impl ApiRequestErased {
98    pub fn new<T: Serialize>(params: T) -> Self {
99        Self {
100            auth: None,
101            params: serde_json::to_value(params)
102                .expect("parameter serialization error - this should not happen"),
103        }
104    }
105
106    pub fn to_json(&self) -> JsonValue {
107        serde_json::to_value(self).expect("parameter serialization error - this should not happen")
108    }
109
110    pub fn with_auth(self, auth: ApiAuth) -> Self {
111        Self {
112            auth: Some(auth),
113            params: self.params,
114        }
115    }
116
117    pub fn to_typed<T: serde::de::DeserializeOwned>(
118        self,
119    ) -> Result<ApiRequest<T>, serde_json::Error> {
120        Ok(ApiRequest {
121            auth: self.auth,
122            params: serde_json::from_value::<T>(self.params)?,
123        })
124    }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub enum ApiMethod {
129    Core(String),
130    Module(ModuleInstanceId, String),
131}
132
133impl fmt::Display for ApiMethod {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            Self::Core(s) => f.write_str(s),
137            Self::Module(module_id, s) => f.write_fmt(format_args!("{module_id}-{s}")),
138        }
139    }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct IrohApiRequest {
144    pub method: ApiMethod,
145    pub request: ApiRequestErased,
146}
147
148pub const FEDIMINT_API_ALPN: &[u8] = b"FEDIMINT_API_ALPN";
149
150/// Authentication uses the hashed user password in PHC format
151#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct ApiAuth(pub String);
153
154impl Debug for ApiAuth {
155    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
156        write!(f, "ApiAuth(****)")
157    }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ApiError {
162    pub code: i32,
163    pub message: String,
164}
165
166impl ApiError {
167    pub fn new(code: i32, message: String) -> Self {
168        Self { code, message }
169    }
170
171    pub fn not_found(message: String) -> Self {
172        Self::new(404, message)
173    }
174
175    pub fn bad_request(message: String) -> Self {
176        Self::new(400, message)
177    }
178
179    pub fn unauthorized() -> Self {
180        Self::new(401, "Invalid authorization".to_string())
181    }
182
183    pub fn server_error(message: String) -> Self {
184        Self::new(500, message)
185    }
186}
187
188/// State made available to all API endpoints for handling a request
189pub struct ApiEndpointContext<'dbtx> {
190    db: Database,
191    dbtx: DatabaseTransaction<'dbtx, Committable>,
192    has_auth: bool,
193    request_auth: Option<ApiAuth>,
194}
195
196impl<'a> ApiEndpointContext<'a> {
197    /// `db` and `dbtx` should be isolated.
198    pub fn new(
199        db: Database,
200        dbtx: DatabaseTransaction<'a, Committable>,
201        has_auth: bool,
202        request_auth: Option<ApiAuth>,
203    ) -> Self {
204        Self {
205            db,
206            dbtx,
207            has_auth,
208            request_auth,
209        }
210    }
211
212    /// Database tx handle, will be committed
213    pub fn dbtx<'s, 'mtx>(&'s mut self) -> DatabaseTransaction<'mtx, NonCommittable>
214    where
215        'a: 'mtx,
216        's: 'mtx,
217    {
218        // dbtx is already isolated.
219        self.dbtx.to_ref_nc()
220    }
221
222    /// Returns the auth set on the request (regardless of whether it was
223    /// correct)
224    pub fn request_auth(&self) -> Option<ApiAuth> {
225        self.request_auth.clone()
226    }
227
228    /// Whether the request was authenticated as the guardian who controls this
229    /// fedimint server
230    pub fn has_auth(&self) -> bool {
231        self.has_auth
232    }
233
234    pub fn db(&self) -> Database {
235        self.db.clone()
236    }
237
238    /// Waits for key to be present in database.
239    pub fn wait_key_exists<K>(&self, key: K) -> impl Future<Output = K::Value> + use<K>
240    where
241        K: DatabaseKey + DatabaseRecord + DatabaseKeyWithNotify,
242    {
243        let db = self.db.clone();
244        // self contains dbtx which is !Send
245        // try removing this and see the error.
246        async move { db.wait_key_exists(&key).await }
247    }
248
249    /// Waits for key to have a value that matches.
250    pub fn wait_value_matches<K>(
251        &self,
252        key: K,
253        matcher: impl Fn(&K::Value) -> bool + Copy,
254    ) -> impl Future<Output = K::Value>
255    where
256        K: DatabaseKey + DatabaseRecord + DatabaseKeyWithNotify,
257    {
258        let db = self.db.clone();
259        async move { db.wait_key_check(&key, |v| v.filter(matcher)).await.0 }
260    }
261
262    /// Attempts to commit the dbtx or returns an `ApiError`
263    pub async fn commit_tx_result(self, path: &'static str) -> Result<(), ApiError> {
264        self.dbtx.commit_tx_result().await.map_err(|err| {
265            tracing::warn!(
266                target: fedimint_logging::LOG_NET_API,
267                path,
268                "API server error when writing to database: {:?}",
269                err
270            );
271            ApiError {
272                code: 500,
273                message: "API server error when writing to database".to_string(),
274            }
275        })
276    }
277}
278
279#[apply(async_trait_maybe_send!)]
280pub trait TypedApiEndpoint {
281    type State: Sync;
282
283    /// example: /transaction
284    const PATH: &'static str;
285
286    type Param: serde::de::DeserializeOwned + Send;
287    type Response: serde::Serialize;
288
289    async fn handle<'state, 'context, 'dbtx>(
290        state: &'state Self::State,
291        context: &'context mut ApiEndpointContext<'dbtx>,
292        request: Self::Param,
293    ) -> Result<Self::Response, ApiError>
294    where
295        'dbtx: 'context;
296}
297
298pub use serde_json;
299
300/// # Example
301///
302/// ```rust
303/// # use fedimint_core::module::ApiVersion;
304/// # use fedimint_core::module::{api_endpoint, ApiEndpoint, registry::ModuleInstanceId};
305/// struct State;
306///
307/// let _: ApiEndpoint<State> = api_endpoint! {
308///     "/foobar",
309///     ApiVersion::new(0, 3),
310///     async |state: &State, _dbtx, params: ()| -> i32 {
311///         Ok(0)
312///     }
313/// };
314/// ```
315#[macro_export]
316macro_rules! __api_endpoint {
317    (
318        $path:expr_2021,
319        // Api Version this endpoint was introduced in, at the current consensus level
320        // Currently for documentation purposes only.
321        $version_introduced:expr_2021,
322        async |$state:ident: &$state_ty:ty, $context:ident, $param:ident: $param_ty:ty| -> $resp_ty:ty $body:block
323    ) => {{
324        struct Endpoint;
325
326        #[$crate::apply($crate::async_trait_maybe_send!)]
327        impl $crate::module::TypedApiEndpoint for Endpoint {
328            #[allow(deprecated)]
329            const PATH: &'static str = $path;
330            type State = $state_ty;
331            type Param = $param_ty;
332            type Response = $resp_ty;
333
334            async fn handle<'state, 'context, 'dbtx>(
335                $state: &'state Self::State,
336                $context: &'context mut $crate::module::ApiEndpointContext<'dbtx>,
337                $param: Self::Param,
338            ) -> ::std::result::Result<Self::Response, $crate::module::ApiError> {
339                {
340                    // just to enforce the correct type
341                    const __API_VERSION: $crate::module::ApiVersion = $version_introduced;
342                }
343                $body
344            }
345        }
346
347        $crate::module::ApiEndpoint::from_typed::<Endpoint>()
348    }};
349}
350
351pub use __api_endpoint as api_endpoint;
352use fedimint_core::NumPeers;
353
354use self::registry::ModuleDecoderRegistry;
355
356type HandlerFnReturn<'a> =
357    Pin<Box<maybe_add_send!(dyn Future<Output = Result<serde_json::Value, ApiError>> + 'a)>>;
358type HandlerFn<M> = Box<
359    maybe_add_send_sync!(
360        dyn for<'a> Fn(&'a M, ApiEndpointContext<'a>, ApiRequestErased) -> HandlerFnReturn<'a>
361    ),
362>;
363
364/// Definition of an API endpoint defined by a module `M`.
365pub struct ApiEndpoint<M> {
366    /// Path under which the API endpoint can be reached. It should start with a
367    /// `/` e.g. `/transaction`. E.g. this API endpoint would be reachable
368    /// under `module_module_instance_id_transaction` depending on the
369    /// module name returned by `[FedertionModule::api_base_name]`.
370    pub path: &'static str,
371    /// Handler for the API call that takes the following arguments:
372    ///   * Reference to the module which defined it
373    ///   * Request parameters parsed into JSON `[Value](serde_json::Value)`
374    pub handler: HandlerFn<M>,
375}
376
377/// Global request ID used for logging
378static REQ_ID: AtomicU64 = AtomicU64::new(0);
379
380// <()> is used to avoid specify state.
381impl ApiEndpoint<()> {
382    pub fn from_typed<E: TypedApiEndpoint>() -> ApiEndpoint<E::State>
383    where
384        <E as TypedApiEndpoint>::Response: MaybeSend,
385        E::Param: Debug,
386        E::Response: Debug,
387    {
388        async fn handle_request<'state, 'context, 'dbtx, E>(
389            state: &'state E::State,
390            context: &'context mut ApiEndpointContext<'dbtx>,
391            request: ApiRequest<E::Param>,
392        ) -> Result<E::Response, ApiError>
393        where
394            'dbtx: 'context,
395            E: TypedApiEndpoint,
396            E::Param: Debug,
397            E::Response: Debug,
398        {
399            tracing::debug!(target: LOG_NET_API, path = E::PATH, ?request, "received api request");
400            let result = E::handle(state, context, request.params).await;
401            match &result {
402                Err(error) => {
403                    tracing::warn!(target: LOG_NET_API, path = E::PATH, ?error, "api request error");
404                }
405                _ => {
406                    tracing::trace!(target: LOG_NET_API, path = E::PATH, "api request complete");
407                }
408            }
409            result
410        }
411
412        ApiEndpoint {
413            path: E::PATH,
414            handler: Box::new(|m, mut context, request| {
415                Box::pin(async {
416                    let request = request
417                        .to_typed()
418                        .map_err(|e| ApiError::bad_request(e.to_string()))?;
419
420                    let span = tracing::info_span!(
421                        target: LOG_NET_API,
422                        "api_req",
423                        id = REQ_ID.fetch_add(1, Ordering::SeqCst),
424                        method = E::PATH,
425                    );
426                    let ret = handle_request::<E>(m, &mut context, request)
427                        .instrument(span)
428                        .await?;
429
430                    context.commit_tx_result(E::PATH).await?;
431
432                    Ok(serde_json::to_value(ret).expect("encoding error"))
433                })
434            }),
435        }
436    }
437}
438
439/// Operations common to Server and Client side module gen dyn newtypes
440///
441/// Due to conflict of `impl Trait for T` for both `ServerModuleInit` and
442/// `ClientModuleInit`, we can't really have a `ICommonModuleInit`, so to unify
443/// them in `ModuleInitRegistry` we move the common functionality to be an
444/// interface over their dyn newtype wrappers. A bit weird, but works.
445#[apply(async_trait_maybe_send!)]
446pub trait IDynCommonModuleInit: Debug {
447    fn decoder(&self) -> Decoder;
448
449    fn module_kind(&self) -> ModuleKind;
450
451    fn to_dyn_common(&self) -> DynCommonModuleInit;
452
453    async fn dump_database(
454        &self,
455        dbtx: &mut DatabaseTransaction<'_>,
456        prefix_names: Vec<String>,
457    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_>;
458}
459
460/// Trait implemented by every `*ModuleInit` (server or client side)
461pub trait ModuleInit: Debug + Clone + Send + Sync + 'static {
462    type Common: CommonModuleInit;
463
464    fn dump_database(
465        &self,
466        dbtx: &mut DatabaseTransaction<'_>,
467        prefix_names: Vec<String>,
468    ) -> maybe_add_send!(
469        impl Future<
470            Output = Box<
471                dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_,
472            >,
473        >
474    );
475}
476
477#[apply(async_trait_maybe_send!)]
478impl<T> IDynCommonModuleInit for T
479where
480    T: ModuleInit,
481{
482    fn decoder(&self) -> Decoder {
483        T::Common::decoder()
484    }
485
486    fn module_kind(&self) -> ModuleKind {
487        T::Common::KIND
488    }
489
490    fn to_dyn_common(&self) -> DynCommonModuleInit {
491        DynCommonModuleInit::from_inner(Arc::new(self.clone()))
492    }
493
494    async fn dump_database(
495        &self,
496        dbtx: &mut DatabaseTransaction<'_>,
497        prefix_names: Vec<String>,
498    ) -> Box<dyn Iterator<Item = (String, Box<dyn erased_serde::Serialize + Send>)> + '_> {
499        <Self as ModuleInit>::dump_database(self, dbtx, prefix_names).await
500    }
501}
502
503dyn_newtype_define!(
504    #[derive(Clone)]
505    pub DynCommonModuleInit(Arc<IDynCommonModuleInit>)
506);
507
508impl AsRef<maybe_add_send_sync!(dyn IDynCommonModuleInit + 'static)> for DynCommonModuleInit {
509    fn as_ref(&self) -> &(maybe_add_send_sync!(dyn IDynCommonModuleInit + 'static)) {
510        self.inner.as_ref()
511    }
512}
513
514impl DynCommonModuleInit {
515    pub fn from_inner(
516        inner: Arc<maybe_add_send_sync!(dyn IDynCommonModuleInit + 'static)>,
517    ) -> Self {
518        Self { inner }
519    }
520}
521
522/// Logic and constant common between server side and client side modules
523#[apply(async_trait_maybe_send!)]
524pub trait CommonModuleInit: Debug + Sized {
525    const CONSENSUS_VERSION: ModuleConsensusVersion;
526    const KIND: ModuleKind;
527
528    type ClientConfig: ClientConfig;
529
530    fn decoder() -> Decoder;
531}
532
533/// Module associated types required by both client and server
534pub trait ModuleCommon {
535    type ClientConfig: ClientConfig;
536    type Input: Input;
537    type Output: Output;
538    type OutputOutcome: OutputOutcome;
539    type ConsensusItem: ModuleConsensusItem;
540    type InputError: InputError;
541    type OutputError: OutputError;
542
543    fn decoder_builder() -> DecoderBuilder {
544        let mut decoder_builder = Decoder::builder();
545        decoder_builder.with_decodable_type::<Self::ClientConfig>();
546        decoder_builder.with_decodable_type::<Self::Input>();
547        decoder_builder.with_decodable_type::<Self::Output>();
548        decoder_builder.with_decodable_type::<Self::OutputOutcome>();
549        decoder_builder.with_decodable_type::<Self::ConsensusItem>();
550        decoder_builder.with_decodable_type::<Self::InputError>();
551        decoder_builder.with_decodable_type::<Self::OutputError>();
552
553        decoder_builder
554    }
555
556    fn decoder() -> Decoder {
557        Self::decoder_builder().build()
558    }
559}
560
561/// Creates a struct that can be used to make our module-decodable structs
562/// interact with `serde`-based APIs (AlephBFT, jsonrpsee). It creates a wrapper
563/// that holds the data as serialized
564// bytes internally.
565#[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
566pub struct SerdeModuleEncoding<T: Encodable + Decodable>(
567    #[serde(with = "::fedimint_core::encoding::as_hex")] Vec<u8>,
568    #[serde(skip)] PhantomData<T>,
569);
570
571/// Same as [`SerdeModuleEncoding`] but uses base64 instead of hex encoding.
572#[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
573pub struct SerdeModuleEncodingBase64<T: Encodable + Decodable>(
574    #[serde(with = "::fedimint_core::encoding::as_base64")] Vec<u8>,
575    #[serde(skip)] PhantomData<T>,
576);
577
578impl<T> fmt::Debug for SerdeModuleEncoding<T>
579where
580    T: Encodable + Decodable,
581{
582    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
583        f.write_str("SerdeModuleEncoding(")?;
584        fmt::Debug::fmt(&AbbreviateHexBytes(&self.0), f)?;
585        f.write_str(")")?;
586        Ok(())
587    }
588}
589
590impl<T: Encodable + Decodable> From<&T> for SerdeModuleEncoding<T> {
591    fn from(value: &T) -> Self {
592        let mut bytes = vec![];
593        fedimint_core::encoding::Encodable::consensus_encode(value, &mut bytes)
594            .expect("Writing to buffer can never fail");
595        Self(bytes, PhantomData)
596    }
597}
598
599impl<T: Encodable + Decodable + 'static> SerdeModuleEncoding<T> {
600    pub fn try_into_inner(&self, modules: &ModuleDecoderRegistry) -> Result<T, DecodeError> {
601        Decodable::consensus_decode_whole(&self.0, modules)
602    }
603
604    /// In cases where we know exactly which module kind we expect but don't
605    /// have access to all decoders this function can be used instead.
606    ///
607    /// Note that it just assumes the decoded module instance id to be valid
608    /// since it cannot validate against the decoder registry. The lack of
609    /// access to a decoder registry also makes decoding structs impossible that
610    /// themselves contain module dyn-types (e.g. a module output containing a
611    /// fedimint transaction).
612    pub fn try_into_inner_known_module_kind(&self, decoder: &Decoder) -> Result<T, DecodeError> {
613        let mut reader = std::io::Cursor::new(&self.0);
614        let module_instance = ModuleInstanceId::consensus_decode_partial(
615            &mut reader,
616            &ModuleDecoderRegistry::default(),
617        )?;
618
619        let total_len =
620            u64::consensus_decode_partial(&mut reader, &ModuleDecoderRegistry::default())?;
621
622        // No recursive module decoding is supported since we give an empty decoder
623        // registry to the decode function
624        decoder.decode_complete(
625            &mut reader,
626            total_len,
627            module_instance,
628            &ModuleRegistry::default(),
629        )
630    }
631}
632
633impl<T> fmt::Debug for SerdeModuleEncodingBase64<T>
634where
635    T: Encodable + Decodable,
636{
637    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
638        f.write_str("SerdeModuleEncoding2(")?;
639        fmt::Debug::fmt(&AbbreviateHexBytes(&self.0), f)?;
640        f.write_str(")")?;
641        Ok(())
642    }
643}
644
645impl<T: Encodable + Decodable> From<&T> for SerdeModuleEncodingBase64<T> {
646    fn from(value: &T) -> Self {
647        let mut bytes = vec![];
648        fedimint_core::encoding::Encodable::consensus_encode(value, &mut bytes)
649            .expect("Writing to buffer can never fail");
650        Self(bytes, PhantomData)
651    }
652}
653
654impl<T: Encodable + Decodable + 'static> SerdeModuleEncodingBase64<T> {
655    pub fn try_into_inner(&self, modules: &ModuleDecoderRegistry) -> Result<T, DecodeError> {
656        Decodable::consensus_decode_whole(&self.0, modules)
657    }
658
659    /// In cases where we know exactly which module kind we expect but don't
660    /// have access to all decoders this function can be used instead.
661    ///
662    /// Note that it just assumes the decoded module instance id to be valid
663    /// since it cannot validate against the decoder registry. The lack of
664    /// access to a decoder registry also makes decoding structs impossible that
665    /// themselves contain module dyn-types (e.g. a module output containing a
666    /// fedimint transaction).
667    pub fn try_into_inner_known_module_kind(&self, decoder: &Decoder) -> Result<T, DecodeError> {
668        let mut reader = std::io::Cursor::new(&self.0);
669        let module_instance = ModuleInstanceId::consensus_decode_partial(
670            &mut reader,
671            &ModuleDecoderRegistry::default(),
672        )?;
673
674        let total_len =
675            u64::consensus_decode_partial(&mut reader, &ModuleDecoderRegistry::default())?;
676
677        // No recursive module decoding is supported since we give an empty decoder
678        // registry to the decode function
679        decoder.decode_complete(
680            &mut reader,
681            total_len,
682            module_instance,
683            &ModuleRegistry::default(),
684        )
685    }
686}
687
688/// A handle passed to `ServerModuleInit::distributed_gen`
689///
690/// This struct encapsulates dkg data that the module should not have a direct
691/// access to, and implements higher level dkg operations available to the
692/// module to complete its distributed initialization inside the federation.
693#[non_exhaustive]
694pub struct PeerHandle<'a> {
695    // TODO: this whole type should be a part of a `fedimint-server` and fields here inaccessible
696    // to outside crates, but until `ServerModule` is not in `fedimint-server` this is impossible
697    #[doc(hidden)]
698    pub num_peers: NumPeers,
699    #[doc(hidden)]
700    pub identity: PeerId,
701    #[doc(hidden)]
702    pub connections: &'a DynP2PConnections<P2PMessage>,
703}
704
705impl<'a> PeerHandle<'a> {
706    pub fn new(
707        num_peers: NumPeers,
708        identity: PeerId,
709        connections: &'a DynP2PConnections<P2PMessage>,
710    ) -> Self {
711        Self {
712            num_peers,
713            identity,
714            connections,
715        }
716    }
717
718    pub fn num_peers(&self) -> NumPeers {
719        self.num_peers
720    }
721}