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 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 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 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 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
129pub 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 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 pub fn new(provider: impl Provider<PodNetwork> + 'static) -> Self {
165 Self {
166 inner: Arc::new(provider),
167 }
168 }
169
170 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 let first_notification = subscription.into_stream().next().await;
218 if first_notification.is_some() {
219 break;
220 }
221 }
222 Ok(())
223 }
224
225 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 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,)) .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}