spl_token_client/
client.rs

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
16/// Basic trait for sending transactions to validator.
17pub trait SendTransaction {
18    type Output;
19}
20
21/// Basic trait for simulating transactions in a validator.
22pub trait SimulateTransaction {
23    type SimulationOutput: SimulationResult;
24}
25
26/// Trait for the output of a simulation
27pub trait SimulationResult {
28    fn get_compute_units_consumed(&self) -> ProgramClientResult<u64>;
29}
30
31/// Extends basic `SendTransaction` trait with function `send` where client is
32/// `&mut BanksClient`. Required for `ProgramBanksClient`.
33pub 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
41/// Extends basic `SimulateTransaction` trait with function `simulation` where
42/// client is `&mut BanksClient`. Required for `ProgramBanksClient`.
43pub 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/// Send transaction to validator using `BanksClient::process_transaction`.
52#[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
102/// Extends basic `SendTransaction` trait with function `send` where client is
103/// `&RpcClient`. Required for `ProgramRpcClient`.
104pub 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
112/// Extends basic `SimulateTransaction` trait with function `simulate` where
113/// client is `&RpcClient`. Required for `ProgramRpcClient`.
114pub 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            // `Transaction` is the result of an offline simulation. The error
160            // should be properly handled by a caller that supports offline
161            // signing
162            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/// Generic client interface for programs.
194#[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
221/// Program client for `BanksClient` from crate `solana-program-test`.
222pub 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
315/// Program client for `RpcClient` from crate `solana-client`.
316pub 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
372/// Program client for offline signing.
373pub 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}