spl_token_client/
client.rs

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
21/// Basic trait for sending transactions to validator.
22pub trait SendTransaction {
23    type Output;
24}
25
26/// Basic trait for simulating transactions in a validator.
27pub trait SimulateTransaction {
28    type SimulationOutput: SimulationResult;
29}
30
31/// Trait for the output of a simulation
32pub trait SimulationResult {
33    fn get_compute_units_consumed(&self) -> ProgramClientResult<u64>;
34}
35
36/// Extends basic `SendTransaction` trait with function `send` where client is
37/// `&mut BanksClient`. Required for `ProgramBanksClient`.
38#[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/// Extends basic `SimulateTransaction` trait with function `simulation` where
48/// client is `&mut BanksClient`. Required for `ProgramBanksClient`.
49#[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/// Send transaction to validator using `BanksClient::process_transaction`.
59#[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
115/// Extends basic `SendTransaction` trait with function `send` where client is
116/// `&RpcClient`. Required for `ProgramRpcClient`.
117pub 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
125/// Extends basic `SimulateTransaction` trait with function `simulate` where
126/// client is `&RpcClient`. Required for `ProgramRpcClient`.
127pub 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            // `Transaction` is the result of an offline simulation. The error
174            // should be properly handled by a caller that supports offline
175            // signing
176            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/// Generic client interface for programs.
208#[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/// Program client for `BanksClient` from crate `solana-program-test`.
237#[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
334/// Program client for `RpcClient` from crate `solana-client`.
335pub 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
391/// Program client for offline signing.
392pub 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}