1use {
2 async_trait::async_trait,
3 solana_account::Account,
4 solana_hash::Hash,
5 solana_pubkey::Pubkey,
6 solana_rpc_client::nonblocking::rpc_client::RpcClient,
7 solana_rpc_client_api::response::RpcSimulateTransactionResult,
8 solana_signature::Signature,
9 solana_transaction::Transaction,
10 std::{fmt, future::Future, pin::Pin, sync::Arc},
11};
12
13#[cfg(feature = "dev-context-only-utils")]
14use {
15 solana_banks_client::BanksClient, solana_banks_interface::BanksTransactionResultWithSimulation,
16 solana_program_test::ProgramTestContext, tokio::sync::Mutex,
17};
18
19type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
20
21pub trait SendTransaction {
23 type Output;
24}
25
26pub trait SimulateTransaction {
28 type SimulationOutput: SimulationResult;
29}
30
31pub trait SimulationResult {
33 fn get_compute_units_consumed(&self) -> ProgramClientResult<u64>;
34}
35
36#[cfg(feature = "dev-context-only-utils")]
39pub trait SendTransactionBanksClient: SendTransaction {
40 fn send<'a>(
41 &self,
42 client: &'a mut BanksClient,
43 transaction: Transaction,
44 ) -> BoxFuture<'a, ProgramClientResult<Self::Output>>;
45}
46
47#[cfg(feature = "dev-context-only-utils")]
50pub trait SimulateTransactionBanksClient: SimulateTransaction {
51 fn simulate<'a>(
52 &self,
53 client: &'a mut BanksClient,
54 transaction: Transaction,
55 ) -> BoxFuture<'a, ProgramClientResult<Self::SimulationOutput>>;
56}
57
58#[derive(Debug, Clone, Copy, Default)]
60#[cfg(feature = "dev-context-only-utils")]
61pub struct ProgramBanksClientProcessTransaction;
62
63#[cfg(feature = "dev-context-only-utils")]
64impl SendTransaction for ProgramBanksClientProcessTransaction {
65 type Output = ();
66}
67
68#[cfg(feature = "dev-context-only-utils")]
69impl SendTransactionBanksClient for ProgramBanksClientProcessTransaction {
70 fn send<'a>(
71 &self,
72 client: &'a mut BanksClient,
73 transaction: Transaction,
74 ) -> BoxFuture<'a, ProgramClientResult<Self::Output>> {
75 Box::pin(async move {
76 client
77 .process_transaction(transaction)
78 .await
79 .map_err(Into::into)
80 })
81 }
82}
83
84#[cfg(feature = "dev-context-only-utils")]
85impl SimulationResult for BanksTransactionResultWithSimulation {
86 fn get_compute_units_consumed(&self) -> ProgramClientResult<u64> {
87 self.simulation_details
88 .as_ref()
89 .map(|x| x.units_consumed)
90 .ok_or("No simulation results found".into())
91 }
92}
93
94#[cfg(feature = "dev-context-only-utils")]
95impl SimulateTransaction for ProgramBanksClientProcessTransaction {
96 type SimulationOutput = BanksTransactionResultWithSimulation;
97}
98
99#[cfg(feature = "dev-context-only-utils")]
100impl SimulateTransactionBanksClient for ProgramBanksClientProcessTransaction {
101 fn simulate<'a>(
102 &self,
103 client: &'a mut BanksClient,
104 transaction: Transaction,
105 ) -> BoxFuture<'a, ProgramClientResult<Self::SimulationOutput>> {
106 Box::pin(async move {
107 client
108 .simulate_transaction(transaction)
109 .await
110 .map_err(Into::into)
111 })
112 }
113}
114
115pub trait SendTransactionRpc: SendTransaction {
118 fn send<'a>(
119 &self,
120 client: &'a RpcClient,
121 transaction: &'a Transaction,
122 ) -> BoxFuture<'a, ProgramClientResult<Self::Output>>;
123}
124
125pub trait SimulateTransactionRpc: SimulateTransaction {
128 fn simulate<'a>(
129 &self,
130 client: &'a RpcClient,
131 transaction: &'a Transaction,
132 ) -> BoxFuture<'a, ProgramClientResult<Self::SimulationOutput>>;
133}
134
135#[derive(Debug, Clone, Copy, Default)]
136pub struct ProgramRpcClientSendTransaction;
137
138#[derive(Debug, Clone, PartialEq)]
139#[allow(clippy::large_enum_variant)]
140pub enum RpcClientResponse {
141 Signature(Signature),
142 Transaction(Transaction),
143 Simulation(RpcSimulateTransactionResult),
144}
145
146impl SendTransaction for ProgramRpcClientSendTransaction {
147 type Output = RpcClientResponse;
148}
149
150impl SendTransactionRpc for ProgramRpcClientSendTransaction {
151 fn send<'a>(
152 &self,
153 client: &'a RpcClient,
154 transaction: &'a Transaction,
155 ) -> BoxFuture<'a, ProgramClientResult<Self::Output>> {
156 Box::pin(async move {
157 if !transaction.is_signed() {
158 return Err("Cannot send transaction: not fully signed".into());
159 }
160
161 client
162 .send_and_confirm_transaction(transaction)
163 .await
164 .map(RpcClientResponse::Signature)
165 .map_err(Into::into)
166 })
167 }
168}
169
170impl SimulationResult for RpcClientResponse {
171 fn get_compute_units_consumed(&self) -> ProgramClientResult<u64> {
172 match self {
173 Self::Signature(_) | Self::Transaction(_) => Err("Not a simulation result".into()),
177 Self::Simulation(simulation_result) => simulation_result
178 .units_consumed
179 .ok_or("No simulation results found".into()),
180 }
181 }
182}
183
184impl SimulateTransaction for ProgramRpcClientSendTransaction {
185 type SimulationOutput = RpcClientResponse;
186}
187
188impl SimulateTransactionRpc for ProgramRpcClientSendTransaction {
189 fn simulate<'a>(
190 &self,
191 client: &'a RpcClient,
192 transaction: &'a Transaction,
193 ) -> BoxFuture<'a, ProgramClientResult<Self::SimulationOutput>> {
194 Box::pin(async move {
195 client
196 .simulate_transaction(transaction)
197 .await
198 .map(|r| RpcClientResponse::Simulation(r.value))
199 .map_err(Into::into)
200 })
201 }
202}
203
204pub type ProgramClientError = Box<dyn std::error::Error + Send + Sync>;
205pub type ProgramClientResult<T> = Result<T, ProgramClientError>;
206
207#[async_trait]
209pub trait ProgramClient<ST>
210where
211 ST: SendTransaction + SimulateTransaction,
212{
213 async fn get_minimum_balance_for_rent_exemption(
214 &self,
215 data_len: usize,
216 ) -> ProgramClientResult<u64>;
217
218 async fn get_latest_blockhash(&self) -> ProgramClientResult<Hash>;
219
220 async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult<ST::Output>;
221
222 async fn get_account(&self, address: Pubkey) -> ProgramClientResult<Option<Account>>;
223
224 async fn simulate_transaction(
225 &self,
226 transaction: &Transaction,
227 ) -> ProgramClientResult<ST::SimulationOutput>;
228}
229
230#[cfg(feature = "dev-context-only-utils")]
231enum ProgramBanksClientContext {
232 Client(Arc<Mutex<BanksClient>>),
233 Context(Arc<Mutex<ProgramTestContext>>),
234}
235
236#[cfg(feature = "dev-context-only-utils")]
238pub struct ProgramBanksClient<ST> {
239 context: ProgramBanksClientContext,
240 send: ST,
241}
242
243#[cfg(feature = "dev-context-only-utils")]
244impl<ST> fmt::Debug for ProgramBanksClient<ST> {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 f.debug_struct("ProgramBanksClient").finish()
247 }
248}
249
250#[cfg(feature = "dev-context-only-utils")]
251impl<ST> ProgramBanksClient<ST> {
252 fn new(context: ProgramBanksClientContext, send: ST) -> Self {
253 Self { context, send }
254 }
255
256 pub fn new_from_client(client: Arc<Mutex<BanksClient>>, send: ST) -> Self {
257 Self::new(ProgramBanksClientContext::Client(client), send)
258 }
259
260 pub fn new_from_context(context: Arc<Mutex<ProgramTestContext>>, send: ST) -> Self {
261 Self::new(ProgramBanksClientContext::Context(context), send)
262 }
263
264 async fn run_in_lock<F, O>(&self, f: F) -> O
265 where
266 for<'a> F: Fn(&'a mut BanksClient) -> BoxFuture<'a, O>,
267 {
268 match &self.context {
269 ProgramBanksClientContext::Client(client) => {
270 let mut lock = client.lock().await;
271 f(&mut lock).await
272 }
273 ProgramBanksClientContext::Context(context) => {
274 let mut lock = context.lock().await;
275 f(&mut lock.banks_client).await
276 }
277 }
278 }
279}
280
281#[cfg(feature = "dev-context-only-utils")]
282#[async_trait]
283impl<ST> ProgramClient<ST> for ProgramBanksClient<ST>
284where
285 ST: SendTransactionBanksClient + SimulateTransactionBanksClient + Send + Sync,
286{
287 async fn get_minimum_balance_for_rent_exemption(
288 &self,
289 data_len: usize,
290 ) -> ProgramClientResult<u64> {
291 self.run_in_lock(|client| {
292 Box::pin(async move {
293 let rent = client.get_rent().await?;
294 Ok(rent.minimum_balance(data_len))
295 })
296 })
297 .await
298 }
299
300 async fn get_latest_blockhash(&self) -> ProgramClientResult<Hash> {
301 self.run_in_lock(|client| {
302 Box::pin(async move { client.get_latest_blockhash().await.map_err(Into::into) })
303 })
304 .await
305 }
306
307 async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult<ST::Output> {
308 self.run_in_lock(|client| {
309 let transaction = transaction.clone();
310 self.send.send(client, transaction)
311 })
312 .await
313 }
314
315 async fn simulate_transaction(
316 &self,
317 transaction: &Transaction,
318 ) -> ProgramClientResult<ST::SimulationOutput> {
319 self.run_in_lock(|client| {
320 let transaction = transaction.clone();
321 self.send.simulate(client, transaction)
322 })
323 .await
324 }
325
326 async fn get_account(&self, address: Pubkey) -> ProgramClientResult<Option<Account>> {
327 self.run_in_lock(|client| {
328 Box::pin(async move { client.get_account(address).await.map_err(Into::into) })
329 })
330 .await
331 }
332}
333
334pub struct ProgramRpcClient<ST> {
336 client: Arc<RpcClient>,
337 send: ST,
338}
339
340impl<ST> fmt::Debug for ProgramRpcClient<ST> {
341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342 f.debug_struct("ProgramRpcClient").finish()
343 }
344}
345
346impl<ST> ProgramRpcClient<ST> {
347 pub fn new(client: Arc<RpcClient>, send: ST) -> Self {
348 Self { client, send }
349 }
350}
351
352#[async_trait]
353impl<ST> ProgramClient<ST> for ProgramRpcClient<ST>
354where
355 ST: SendTransactionRpc + SimulateTransactionRpc + Send + Sync,
356{
357 async fn get_minimum_balance_for_rent_exemption(
358 &self,
359 data_len: usize,
360 ) -> ProgramClientResult<u64> {
361 self.client
362 .get_minimum_balance_for_rent_exemption(data_len)
363 .await
364 .map_err(Into::into)
365 }
366
367 async fn get_latest_blockhash(&self) -> ProgramClientResult<Hash> {
368 self.client.get_latest_blockhash().await.map_err(Into::into)
369 }
370
371 async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult<ST::Output> {
372 self.send.send(&self.client, transaction).await
373 }
374
375 async fn simulate_transaction(
376 &self,
377 transaction: &Transaction,
378 ) -> ProgramClientResult<ST::SimulationOutput> {
379 self.send.simulate(&self.client, transaction).await
380 }
381
382 async fn get_account(&self, address: Pubkey) -> ProgramClientResult<Option<Account>> {
383 Ok(self
384 .client
385 .get_account_with_commitment(&address, self.client.commitment())
386 .await?
387 .value)
388 }
389}
390
391pub struct ProgramOfflineClient<ST> {
393 blockhash: Hash,
394 _send: ST,
395}
396
397impl<ST> fmt::Debug for ProgramOfflineClient<ST> {
398 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
399 f.debug_struct("ProgramOfflineClient").finish()
400 }
401}
402
403impl<ST> ProgramOfflineClient<ST> {
404 pub fn new(blockhash: Hash, send: ST) -> Self {
405 Self {
406 blockhash,
407 _send: send,
408 }
409 }
410}
411
412#[async_trait]
413impl<ST> ProgramClient<ST> for ProgramOfflineClient<ST>
414where
415 ST: SendTransaction<Output = RpcClientResponse>
416 + SimulateTransaction<SimulationOutput = RpcClientResponse>
417 + Send
418 + Sync,
419{
420 async fn get_minimum_balance_for_rent_exemption(
421 &self,
422 _data_len: usize,
423 ) -> ProgramClientResult<u64> {
424 Err("Unable to fetch minimum balance for rent exemption in offline mode".into())
425 }
426
427 async fn get_latest_blockhash(&self) -> ProgramClientResult<Hash> {
428 Ok(self.blockhash)
429 }
430
431 async fn send_transaction(&self, transaction: &Transaction) -> ProgramClientResult<ST::Output> {
432 Ok(RpcClientResponse::Transaction(transaction.clone()))
433 }
434
435 async fn simulate_transaction(
436 &self,
437 transaction: &Transaction,
438 ) -> ProgramClientResult<ST::SimulationOutput> {
439 Ok(RpcClientResponse::Transaction(transaction.clone()))
440 }
441
442 async fn get_account(&self, _address: Pubkey) -> ProgramClientResult<Option<Account>> {
443 Err("Unable to fetch account in offline mode".into())
444 }
445}