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