pod_sdk/provider/
mod.rs

1use std::sync::Arc;
2
3pub use alloy_provider;
4use alloy_rpc_types::TransactionReceipt;
5use anyhow::Context;
6
7use crate::network::{PodNetwork, PodTransactionRequest};
8use alloy_json_rpc::{RpcRecv, RpcSend};
9use alloy_network::{EthereumWallet, Network, NetworkWallet, TransactionBuilder};
10use alloy_provider::{
11    fillers::{JoinFill, RecommendedFillers, TxFiller, WalletFiller},
12    Identity, PendingTransactionBuilder, Provider, ProviderBuilder, ProviderLayer, RootProvider,
13    SendableTx,
14};
15use alloy_pubsub::Subscription;
16use async_trait::async_trait;
17
18use alloy_transport::{TransportError, TransportResult};
19use futures::StreamExt;
20use pod_types::{
21    consensus::Committee,
22    ledger::log::VerifiableLog,
23    metadata::{MetadataWrappedItem, RegularReceiptMetadata},
24    pagination::{ApiPaginatedResult, CursorPaginationRequest},
25    rpc::filter::LogFilter,
26};
27
28use alloy_primitives::{Address, U256};
29
30use pod_types::Timestamp;
31
32pub struct PodProviderBuilder<L, F>(ProviderBuilder<L, F, PodNetwork>);
33
34impl
35    PodProviderBuilder<
36        Identity,
37        JoinFill<Identity, <PodNetwork as RecommendedFillers>::RecommendedFillers>,
38    >
39{
40    /// Create a PodProviderBuilder set up with recommended settings.
41    ///
42    /// The builder can be used to build a [Provider] configured for the [PodNetwork].
43    ///
44    /// The returned builder has fillers preconfigured to automatically fill
45    /// chain ID, nonce and gas price. Check [PodNetwork::RecommendedFillers] for details.
46    pub fn with_recommended_settings() -> Self {
47        Self(PodProviderBuilder::default().0.with_recommended_fillers())
48    }
49}
50
51impl Default for PodProviderBuilder<Identity, Identity> {
52    fn default() -> Self {
53        Self(ProviderBuilder::<_, _, PodNetwork>::default())
54    }
55}
56
57impl PodProviderBuilder<Identity, Identity> {
58    pub fn new() -> Self {
59        Self::default()
60    }
61}
62
63impl<L, F> PodProviderBuilder<L, F> {
64    /// Finish the layer stack by providing a url for connection,
65    /// outputting the final [`PodProvider`] type with all stack
66    /// components.
67    pub async fn on_url<U: AsRef<str>>(self, url: U) -> Result<PodProvider, TransportError>
68    where
69        L: ProviderLayer<RootProvider<PodNetwork>, PodNetwork>,
70        F: TxFiller<PodNetwork> + ProviderLayer<L::Provider, PodNetwork>,
71        F::Provider: 'static,
72    {
73        let alloy_provider = self.0.connect(url.as_ref()).await?;
74        Ok(PodProvider::new(alloy_provider))
75    }
76
77    /// Configure a wallet to be used for signing transactions and spending funds.
78    pub fn wallet<W>(self, wallet: W) -> PodProviderBuilder<L, JoinFill<F, WalletFiller<W>>>
79    where
80        W: NetworkWallet<PodNetwork>,
81    {
82        PodProviderBuilder::<_, _>(self.0.wallet(wallet))
83    }
84
85    pub fn with_private_key(
86        self,
87        key: crate::SigningKey,
88    ) -> PodProviderBuilder<L, JoinFill<F, WalletFiller<EthereumWallet>>> {
89        let signer = crate::PrivateKeySigner::from_signing_key(key);
90
91        self.wallet(crate::EthereumWallet::new(signer))
92    }
93
94    /// Create [PodProvider] by filling in signer key and RPC url from environment.
95    ///
96    /// The following env variables need to be configured:
97    /// - POD_PRIVATE_KEY: hex-encoded ECDSA private key of the wallet owner
98    /// - POD_RPC_URL: URL for a pod RPC API (example: <https://rpc.dev.pod.network>)
99    ///   (default: ws://127.0.0.1:8545)
100    pub async fn from_env(self) -> anyhow::Result<PodProvider>
101    where
102        L: ProviderLayer<RootProvider<PodNetwork>, PodNetwork>,
103        F: TxFiller<PodNetwork> + ProviderLayer<L::Provider, PodNetwork> + 'static,
104        L::Provider: 'static,
105    {
106        const PK_ENV: &str = "POD_PRIVATE_KEY";
107        fn load_private_key() -> anyhow::Result<crate::SigningKey> {
108            let pk_string = std::env::var(PK_ENV)?;
109            let pk_bytes = hex::decode(pk_string)?;
110            let pk = crate::SigningKey::from_slice(&pk_bytes)?;
111            Ok(pk)
112        }
113        let private_key = load_private_key()
114            .with_context(|| format!("{PK_ENV} env should contain hex-encoded ECDSA signer key"))?;
115
116        let rpc_url = std::env::var("POD_RPC_URL").unwrap_or("ws://127.0.0.1:8545".to_string());
117
118        let provider = self
119            .with_private_key(private_key)
120            .on_url(rpc_url.clone())
121            .await
122            .with_context(|| format!("attaching provider to URL {rpc_url}"))?;
123
124        Ok(provider)
125    }
126}
127
128/// A provider tailored for pod, extending capabilities of alloy [Provider]
129/// with pod-specific features.
130pub struct PodProvider {
131    inner: Arc<dyn Provider<PodNetwork>>,
132}
133
134impl Clone for PodProvider {
135    fn clone(&self) -> Self {
136        Self {
137            inner: self.inner.clone(),
138        }
139    }
140}
141
142#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
143#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
144impl Provider<PodNetwork> for PodProvider {
145    fn root(&self) -> &RootProvider<PodNetwork> {
146        self.inner.root()
147    }
148
149    // NOTE: we need to override send_transaction_internal because it is
150    // overriden in [FillProvider], which we use internally in `inner.
151    // Otherwise, we would call the default implementation, which is different.
152    // Perhaps we should do this for all methods?
153    async fn send_transaction_internal(
154        &self,
155        tx: SendableTx<PodNetwork>,
156    ) -> TransportResult<PendingTransactionBuilder<PodNetwork>> {
157        self.inner.send_transaction_internal(tx).await
158    }
159}
160
161impl PodProvider {
162    /// Create a new [PodProvider] using the underlying alloy [Provider].
163    pub fn new(provider: impl Provider<PodNetwork> + 'static) -> Self {
164        Self {
165            inner: Arc::new(provider),
166        }
167    }
168
169    /// Gets the current committee members
170    pub async fn get_committee(&self) -> TransportResult<Committee> {
171        self.client().request_noparams("pod_getCommittee").await
172    }
173
174    pub async fn get_verifiable_logs(
175        &self,
176        filter: &LogFilter,
177    ) -> TransportResult<Vec<VerifiableLog>> {
178        self.client().request("eth_getLogs", (filter,)).await
179    }
180
181    pub async fn websocket_subscribe<Params, Resp>(
182        &self,
183        method: &str,
184        params: Params,
185    ) -> TransportResult<Subscription<Resp>>
186    where
187        Params: RpcSend,
188        Resp: RpcRecv,
189    {
190        let id = self
191            .client()
192            .request("eth_subscribe", (method, params))
193            .await?;
194        self.root().get_subscription(id).await
195    }
196
197    pub async fn subscribe_verifiable_logs(
198        &self,
199        filter: &LogFilter,
200    ) -> TransportResult<Subscription<VerifiableLog>> {
201        self.websocket_subscribe("logs", filter).await
202    }
203
204    pub async fn wait_past_perfect_time(&self, timestamp: Timestamp) -> TransportResult<()> {
205        loop {
206            let subscription: Subscription<String> = self
207                .websocket_subscribe("pod_pastPerfectTime", timestamp.as_micros())
208                .await?;
209            // returns None if connection closes before a notification was sent
210            let first_notification = subscription.into_stream().next().await;
211            if first_notification.is_some() {
212                break;
213            }
214        }
215        Ok(())
216    }
217
218    /// Subscribe to continuously receive TX receipts as they are created on the node.
219    ///
220    /// The parameters `address` and `since` allow to optionally filter receipts.
221    /// Pass `None` and `Timestamp::zero()` respectively for wildcards.
222    pub async fn subscribe_receipts(
223        &self,
224        address: Option<Address>,
225        since: Timestamp,
226    ) -> TransportResult<
227        Subscription<MetadataWrappedItem<TransactionReceipt, RegularReceiptMetadata>>,
228    > {
229        self.websocket_subscribe("pod_receipts", (address, since))
230            .await
231    }
232
233    pub async fn get_receipts(
234        &self,
235        address: Option<Address>,
236        since_micros: u64,
237        paginator: Option<CursorPaginationRequest>,
238    ) -> TransportResult<ApiPaginatedResult<<PodNetwork as Network>::ReceiptResponse>> {
239        self.client()
240            .request("pod_listReceipts", &(address, since_micros, paginator))
241            .await
242    }
243
244    /// Transfer specified `amount` funds to the `to` account.
245    pub async fn transfer(
246        &self,
247        to: Address,
248        amount: U256,
249    ) -> Result<<PodNetwork as Network>::ReceiptResponse, Box<dyn std::error::Error>> {
250        let tx = PodTransactionRequest::default()
251            .with_to(to)
252            .with_value(amount);
253
254        let pending_tx = self.send_transaction(tx).await?;
255
256        let receipt = pending_tx.get_receipt().await?;
257
258        Ok(receipt)
259    }
260}