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